From b2b54729daef0966b470b81f689b296bc09e0f1c Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 6 May 2026 23:29:19 +0200 Subject: [PATCH] feat: add assistant PDF export --- public/components/assistant.html | 1 + public/js/clinicalAssistant.js | 63 +++++++++++++++++++++++++++++++- src/routes/clinicalAssistant.js | 33 +++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/public/components/assistant.html b/public/components/assistant.html index cd43d42..3ccc7fc 100644 --- a/public/components/assistant.html +++ b/public/components/assistant.html @@ -19,6 +19,7 @@
+
diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index 03a712d..900375c 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -53,12 +53,14 @@ var form = document.getElementById('assistant-form'); var clearBtn = document.getElementById('btn-assistant-clear'); var copyBtn = document.getElementById('btn-assistant-copy'); + var exportBtn = document.getElementById('btn-assistant-export-pdf'); var imageBtn = document.getElementById('btn-assistant-image'); var input = document.getElementById('assistant-input'); if (form) form.addEventListener('submit', onAsk); if (clearBtn) clearBtn.addEventListener('click', clearConversation); if (copyBtn) copyBtn.addEventListener('click', copyLastAnswer); + if (exportBtn) exportBtn.addEventListener('click', exportAnswerPdf); if (imageBtn) imageBtn.addEventListener('click', generateImage); if (input) input.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') onAsk(e); @@ -110,7 +112,7 @@ if (input) input.value = ''; if (isImageRequest(text)) { - generateImage(text, true); + prepareSidebarImagePrompt(text); return; } @@ -378,6 +380,26 @@ }); } + function buildContextualImagePrompt(request) { + var prompt = String(request || '').trim(); + if (!lastAnswer) return prompt; + var sourceText = lastSources.slice(0, 6).map(function (s, idx) { + return '[' + (s.number || idx + 1) + '] ' + (s.title || s.resource || 'Source') + (s.page ? ', page ' + s.page : ''); + }).join('\n'); + return prompt + '\n\nUse this clinical answer as the required context. Do not switch topics or introduce unrelated scenes such as gardening. Create a medical teaching visual faithful to the answer.\n\nAnswer:\n' + lastAnswer.slice(0, 3000) + '\n\nSources:\n' + sourceText; + } + + function prepareSidebarImagePrompt(request) { + var promptEl = document.getElementById('assistant-image-prompt'); + var prompt = buildContextualImagePrompt(request); + if (promptEl) { + promptEl.value = prompt; + promptEl.focus(); + } + appendMessage('assistant', 'I prepared the image prompt in the **Image / Graph** box on the right. Click **Generate image** there so the visual uses the current answer as context instead of treating this as a separate chat request.'); + setBusy(false, 'Ready'); + } + function clearConversation() { messages = []; lastAnswer = ''; @@ -435,6 +457,45 @@ }); } + function exportAnswerPdf() { + if (!lastAnswer) { if (typeof showToast === 'function') showToast('No answer to export', 'error'); return; } + setBusy(true, 'Preparing PDF...'); + fetch('/api/clinical-assistant/export-summary', { + method: 'POST', headers: getAuthHeaders(), credentials: 'same-origin', + body: JSON.stringify({ answer: lastAnswer, sources: lastSources }) + }) + .then(function (r) { return r.json().then(function (data) { data._status = r.status; return data; }); }) + .then(function (data) { + setBusy(false, 'Ready'); + openPrintableExport(data.success && data.summary ? data.summary : lastAnswer, lastSources); + }) + .catch(function () { + setBusy(false, 'Ready'); + openPrintableExport(lastAnswer, lastSources); + }); + } + + function openPrintableExport(answer, sources) { + var doc = window.open('', '_blank', 'width=900,height=1100'); + if (!doc) { if (typeof showToast === 'function') showToast('Allow popups to export PDF', 'error'); return; } + var refs = (sources || []).map(function (s, idx) { + var n = s.number || idx + 1; + var title = s.title || s.resource || 'Untitled source'; + var page = s.page || s.page_number || s.pageNumber; + return '
  • [' + n + '] ' + escapeHtml(title) + (page ? ', page ' + escapeHtml(page) : '') + '.
  • '; + }).join(''); + var html = 'Clinical Assistant Export' + + '' + + '

    Clinical Assistant Export

    ' + + '
    Abridged export generated ' + escapeHtml(new Date().toLocaleString()) + '
    ' + + '

    Summary

    ' + renderMarkdown(answer, sources || []) + '
    ' + + '

    References

      ' + refs + '
    '; + doc.document.open(); + doc.document.write(html); + doc.document.close(); + setTimeout(function () { try { doc.focus(); doc.print(); } catch (e) {} }, 500); + } + function isImageRequest(text) { return /\b(generate|create|draw|make|show|render)\b[\s\S]{0,80}\b(image|picture|illustration|visual|infographic|diagram|flowchart)\b/i.test(text) || /\b(image|picture|illustration|visual|infographic|diagram|flowchart)\b[\s\S]{0,80}\b(of|for|about|showing)\b/i.test(text); diff --git a/src/routes/clinicalAssistant.js b/src/routes/clinicalAssistant.js index be4929e..0f38edc 100644 --- a/src/routes/clinicalAssistant.js +++ b/src/routes/clinicalAssistant.js @@ -218,6 +218,39 @@ router.post('/clinical-assistant/image', async function(req, res) { } }); +router.post('/clinical-assistant/export-summary', async function(req, res) { + try { + var answer = String(req.body.answer || '').trim(); + var sources = Array.isArray(req.body.sources) ? req.body.sources.slice(0, 20) : []; + if (!answer) return res.status(400).json({ error: 'Answer is required' }); + + var chatModel = await getSetting('clinical_assistant.chat_model', '') || await getSetting('models.default', ''); + var sourceList = sources.map(function(s, idx) { + var n = s.number || idx + 1; + return '[' + n + '] ' + cleanTitle(s.title || s.resource || 'Source') + (s.page ? ', page ' + s.page : ''); + }).join('\n'); + var ai = await callAI([ + { + role: 'system', + content: 'Create an abridged clinical export summary. Preserve factual meaning. Use only citation numbers already present in the original answer and provided source list. Do not invent, renumber, merge, or move citations. If a claim cannot keep its citation, omit the claim. No references section.' + }, + { + role: 'user', + content: 'Original answer:\n' + answer.substring(0, 8000) + '\n\nAvailable cited sources:\n' + sourceList + '\n\nReturn a concise export version with the same citation numbers where supported.' + } + ], { + model: chatModel || undefined, + temperature: 0.05, + maxTokens: 900 + }); + + var summary = stripModelSourcesSection(String(ai.content || '').trim()); + res.json({ success: true, summary: summary || answer, model: ai.model || chatModel || null }); + } catch (e) { + res.status(500).json({ error: assistantErrorMessage(e) }); + } +}); + async function semanticSearch(query, opts) { var session = await mcpRequest({ jsonrpc: '2.0',