Fix WebDAV selection, pdf-parse, prompt(), CSP, security issues 1/2/4/5
pdf-parse (was v2, broken API): - Downgraded to v1.1.1 — default export is a function again - pdfParse is not a function error fixed WebDAV file selection (style.display bug, same cascade issue): - lh-ai-webdav-selected had inline style="display:flex" always visible - selectWebdavFile() / deselectWebdavFile() now use element.style.display - On select: browser div hides, selected indicator shows with file name + X - On deselect (X): indicator hides, browser shows again Topic context for Upload and Nextcloud tabs: - Both tabs now have optional "Topic / context" field - Sent to backend as 'topic' param to help AI focus the generated content Inline refine bar (replaces window.prompt): - Refine Body button shows/hides an amber inline input bar below generate buttons - Enter instructions, press Apply — no browser dialog, works in all contexts CSP: remove unsafe-inline from scriptSrc (security issue #2): - Converted all 26 onclick= handlers in component HTML files to data-action / data-target / data-label attributes - Added delegated click handler in app.js for copy/speak/nc-export actions - script-src now 'self' only; script-src-attr 'none' WebDAV path endpoint (security issue #5): - New POST /api/user/webdav-path (authMiddleware only, not moderator) - nextcloud.js updated to use new endpoint nodemailer: already at 6.10.1 (vulnerability fixed).
This commit is contained in:
parent
9a584bcc8a
commit
23d39dec65
14 changed files with 147 additions and 253 deletions
236
package-lock.json
generated
236
package-lock.json
generated
|
|
@ -29,7 +29,7 @@
|
|||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"openai": "^4.73.0",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pg": "^8.13.0",
|
||||
"pptxgenjs": "^4.0.1",
|
||||
"qrcode": "^1.5.4",
|
||||
|
|
@ -1010,190 +1010,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
|
||||
"integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.80",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.80",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.80",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.80",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.80",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.80"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
|
||||
"integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
|
||||
"integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
|
||||
"integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
|
||||
"integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
|
||||
"integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
|
||||
"integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
|
||||
"integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.80",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
|
||||
"integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz",
|
||||
|
|
@ -4468,6 +4284,12 @@
|
|||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-ensure": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
|
||||
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
|
|
@ -4743,37 +4565,33 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdf-parse": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz",
|
||||
"integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz",
|
||||
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@napi-rs/canvas": "0.1.80",
|
||||
"pdfjs-dist": "5.4.296"
|
||||
},
|
||||
"bin": {
|
||||
"pdf-parse": "bin/cli.mjs"
|
||||
"debug": "^3.1.0",
|
||||
"node-ensure": "^0.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.16.0 <21 || >=22.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/mehmet-kozan"
|
||||
"node": ">=6.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "5.4.296",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
|
||||
"integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20.16.0 || >=22.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.80"
|
||||
"node_modules/pdf-parse/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"openai": "^4.73.0",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pg": "^8.13.0",
|
||||
"pptxgenjs": "^4.0.1",
|
||||
"qrcode": "^1.5.4",
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@
|
|||
<h3><i class="fas fa-file-medical"></i> Chart Review</h3>
|
||||
<div class="output-actions">
|
||||
<span id="cr-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-primary" onclick="copyText('cr-review-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" onclick="speakText('cr-review-text')"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('cr-review-text','chart-review')"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="cr-review-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="cr-review-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" data-action="nc-export" data-target="cr-review-text" data-label="chart-review"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cr-review-text" class="output-text" contenteditable="true"></div>
|
||||
|
|
|
|||
|
|
@ -157,10 +157,15 @@
|
|||
<small style="color:var(--g400);display:block;margin-top:4px;">PDF, TXT, MD, HTML — max 20 MB</small>
|
||||
<input type="file" id="lh-ai-file" accept=".pdf,.txt,.md,.html,.htm,.csv,.json" style="display:none;">
|
||||
</label>
|
||||
<div class="form-group" style="margin:8px 0 0;">
|
||||
<label style="font-size:12px;font-weight:600;color:var(--g600);">Topic / context <span style="color:var(--g400);font-weight:400;">(optional — helps AI focus)</span></label>
|
||||
<input type="text" id="lh-ai-upload-context" class="cms-input-sm" style="width:100%;" placeholder="e.g., Pediatric asthma management, focus on treatment ladder">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebDAV tab -->
|
||||
<div class="lh-ai-tabpanel hidden" id="lh-ai-tp-webdav">
|
||||
<!-- File browser — hidden once a file is selected -->
|
||||
<div id="lh-ai-webdav-browser">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||||
<span id="lh-ai-webdav-path-label" style="font-size:12px;color:var(--g500);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">/</span>
|
||||
|
|
@ -169,10 +174,17 @@
|
|||
<div id="lh-ai-webdav-list" style="max-height:200px;overflow-y:auto;border:1px solid var(--g200);border-radius:8px;background:white;">
|
||||
<div style="padding:12px;text-align:center;color:var(--g400);font-size:13px;">Click to browse your Nextcloud files</div>
|
||||
</div>
|
||||
<div id="lh-ai-webdav-selected" class="hidden" style="margin-top:8px;padding:8px 12px;background:var(--blue-light);border-radius:6px;font-size:13px;color:var(--blue);display:flex;align-items:center;gap:8px;">
|
||||
</div>
|
||||
<!-- Selected file indicator — shown after selection, replaces browser -->
|
||||
<div id="lh-ai-webdav-selected" style="display:none;flex-direction:column;gap:8px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--blue-light);border-radius:6px;font-size:13px;color:var(--blue);">
|
||||
<i class="fas fa-file-check"></i>
|
||||
<span id="lh-ai-webdav-selected-name" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
|
||||
<button id="btn-lh-webdav-deselect" style="background:none;border:none;cursor:pointer;color:var(--blue);padding:0;font-size:14px;">✕</button>
|
||||
<button id="btn-lh-webdav-deselect" style="background:none;border:none;cursor:pointer;color:var(--blue);padding:0;font-size:16px;line-height:1;" title="Remove selection">✕</button>
|
||||
</div>
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label style="font-size:12px;font-weight:600;color:var(--g600);">Topic / context <span style="color:var(--g400);font-weight:400;">(optional — helps AI focus)</span></label>
|
||||
<input type="text" id="lh-ai-webdav-context" class="cms-input-sm" style="width:100%;" placeholder="e.g., Focus on management guidelines for pediatric residents">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -238,9 +250,15 @@
|
|||
<i class="fas fa-wand-magic-sparkles"></i> Generate Content
|
||||
</button>
|
||||
<button id="btn-lh-ai-refine-body" class="btn-sm btn-ghost" style="padding:10px;">
|
||||
<i class="fas fa-pen-to-square"></i> Refine Current Body
|
||||
<i class="fas fa-pen-to-square"></i> Refine Body
|
||||
</button>
|
||||
</div>
|
||||
<!-- Inline refine input bar — shown when Refine Body is clicked -->
|
||||
<div id="lh-ai-refine-bar" style="display:none;gap:6px;align-items:center;padding:10px 12px;background:#fffbeb;border:1.5px solid var(--amber);border-radius:8px;margin-top:4px;">
|
||||
<input type="text" id="lh-ai-refine-input" class="cms-input-sm" style="flex:1;" placeholder='e.g. "Make it more concise" or "Add a clinical case example"'>
|
||||
<button id="btn-lh-refine-submit" class="btn-sm btn-primary" style="padding:5px 14px;white-space:nowrap;">Apply</button>
|
||||
<button id="btn-lh-refine-cancel" class="btn-sm btn-ghost" style="padding:5px 10px;">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body editor (Tiptap rich text) — hidden for presentations -->
|
||||
|
|
|
|||
|
|
@ -69,9 +69,9 @@
|
|||
<h3><i class="fas fa-file-medical"></i> Output</h3>
|
||||
<div class="output-actions">
|
||||
<span id="dict-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-primary" onclick="copyText('dict-hpi-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" onclick="speakText('dict-hpi-text')"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('dict-hpi-text','hpi-dictation')"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="dict-hpi-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="dict-hpi-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" data-action="nc-export" data-target="dict-hpi-text" data-label="hpi-dictation"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dict-hpi-text" class="output-text" contenteditable="true"></div>
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@
|
|||
<h3><i class="fas fa-file-medical"></i> Generated HPI</h3>
|
||||
<div class="output-actions">
|
||||
<span id="enc-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-primary" onclick="copyText('enc-hpi-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" onclick="speakText('enc-hpi-text')"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('enc-hpi-text','hpi-encounter')"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="enc-hpi-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="enc-hpi-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -138,9 +138,9 @@
|
|||
<div class="output-actions">
|
||||
<span id="hc-format-tag" class="model-tag"></span>
|
||||
<span id="hc-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-primary" onclick="copyText('hc-course-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" onclick="speakText('hc-course-text')"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('hc-course-text','hospital-course')"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="hc-course-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="hc-course-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" data-action="nc-export" data-target="hc-course-text" data-label="hospital-course"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hc-course-text" class="output-text" contenteditable="true"></div>
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@
|
|||
<div class="output-actions">
|
||||
<span id="sick-note-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-ghost" id="btn-sick-new-patient"><i class="fas fa-rotate-left"></i> New Patient</button>
|
||||
<button class="btn-sm btn-primary" onclick="copyText('sick-note-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" onclick="speakText('sick-note-text')"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="sick-note-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="sick-note-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sick-note-text" class="output-text" contenteditable="true"></div>
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@
|
|||
<h3><i class="fas fa-file-medical"></i> SOAP Note</h3>
|
||||
<div class="output-actions">
|
||||
<span id="soap-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-primary" onclick="copyText('soap-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" onclick="speakText('soap-text')"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('soap-text','soap-note')"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="soap-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="soap-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" data-action="nc-export" data-target="soap-text" data-label="soap-note"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="soap-text" class="output-text" contenteditable="true"></div>
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@
|
|||
<h3><i class="fas fa-file-medical"></i> Developmental Assessment</h3>
|
||||
<div class="output-actions">
|
||||
<span id="ms-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-primary" onclick="copyText('ms-narrative-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="ms-narrative-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-success" id="btn-ms-copy-to-note"><i class="fas fa-clipboard-check"></i> Copy to Note</button>
|
||||
<button class="btn-sm btn-ghost" onclick="speakText('ms-narrative-text')"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('ms-narrative-text','milestones')"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="ms-narrative-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" data-action="nc-export" data-target="ms-narrative-text" data-label="milestones"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ms-summary-bar" class="summary-bar hidden"></div>
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
<div class="card-header summary-output-header">
|
||||
<h3><i class="fas fa-clipboard-list"></i> Quick Summary</h3>
|
||||
<div class="output-actions">
|
||||
<button class="btn-sm btn-primary" onclick="copyText('ms-summary-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="ms-summary-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ms-summary-text" class="summary-output-text" contenteditable="true"></div>
|
||||
|
|
@ -149,8 +149,8 @@
|
|||
<div class="output-actions">
|
||||
<span id="shadess-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-ghost" id="btn-shadess-new-patient"><i class="fas fa-rotate-left"></i> New Patient</button>
|
||||
<button class="btn-sm btn-primary" onclick="copyText('shadess-result-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" onclick="speakText('shadess-result-text')"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="shadess-result-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="shadess-result-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="shadess-result-text" class="output-text" contenteditable="true"></div>
|
||||
|
|
@ -290,9 +290,9 @@
|
|||
<div class="output-actions">
|
||||
<span id="wv-note-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-ghost" id="btn-wv-new-patient"><i class="fas fa-rotate-left"></i> New Patient</button>
|
||||
<button class="btn-sm btn-primary" onclick="copyText('wv-note-text')"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" onclick="speakText('wv-note-text')"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('wv-note-text','well-visit')"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="wv-note-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="wv-note-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" data-action="nc-export" data-target="wv-note-text" data-label="well-visit"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wv-note-text" class="output-text" contenteditable="true"></div>
|
||||
|
|
|
|||
|
|
@ -234,6 +234,31 @@ function setOutputText(el, text) {
|
|||
el.innerHTML = safe.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// ── Data-action event delegation (replaces inline onclick handlers) ──
|
||||
// Handles data-action="copy|speak|nc-export" on any element,
|
||||
// allowing removal of 'unsafe-inline' from the Content Security Policy.
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
var action = btn.getAttribute('data-action');
|
||||
var targetId = btn.getAttribute('data-target');
|
||||
if (!action) return;
|
||||
|
||||
if (action === 'copy' && targetId) {
|
||||
if (typeof copyText === 'function') copyText(targetId);
|
||||
return;
|
||||
}
|
||||
if (action === 'speak' && targetId) {
|
||||
if (typeof speakText === 'function') speakText(targetId);
|
||||
return;
|
||||
}
|
||||
if (action === 'nc-export' && targetId) {
|
||||
var label = btn.getAttribute('data-label') || 'export';
|
||||
if (typeof exportToNextcloud === 'function') exportToNextcloud(targetId, label);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Announcement banner ────────────────────────────────────
|
||||
function loadAnnouncement() {
|
||||
fetch('/api/admin/config/announcement', { headers: getAuthHeaders() })
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@
|
|||
if (e.target.closest('#btn-lh-ai-open')) { openAiPanel(); return; }
|
||||
if (e.target.closest('#btn-lh-ai-close')) { closeAiPanel(); return; }
|
||||
if (e.target.closest('#btn-lh-ai-generate')) { runAiGenerate(); return; }
|
||||
if (e.target.closest('#btn-lh-ai-refine-body')) { runAiRefineBody(); return; }
|
||||
if (e.target.closest('#btn-lh-ai-refine-body')) { toggleRefineBar(); return; }
|
||||
if (e.target.closest('#btn-lh-refine-submit')) { submitRefineBody(); return; }
|
||||
if (e.target.closest('#btn-lh-refine-cancel')) { toggleRefineBar(false); return; }
|
||||
if (e.target.closest('#btn-lh-webdav-refresh')) { browseWebdav(_aiWebdavCurrentPath); return; }
|
||||
if (e.target.closest('#btn-lh-webdav-deselect')) { deselectWebdavFile(); return; }
|
||||
|
||||
|
|
@ -671,17 +673,22 @@
|
|||
function selectWebdavFile(path, name) {
|
||||
_aiWebdavSelectedPath = path;
|
||||
_aiWebdavSelectedName = name;
|
||||
var selDiv = document.getElementById('lh-ai-webdav-selected');
|
||||
var browser = document.getElementById('lh-ai-webdav-browser');
|
||||
var selDiv = document.getElementById('lh-ai-webdav-selected');
|
||||
var selName = document.getElementById('lh-ai-webdav-selected-name');
|
||||
if (selDiv) selDiv.classList.remove('hidden');
|
||||
if (selName) selName.textContent = name;
|
||||
// Hide browser, show selected indicator (use style.display — class="hidden" is overridden by inline styles)
|
||||
if (browser) browser.style.display = 'none';
|
||||
if (selDiv) selDiv.style.display = 'flex';
|
||||
if (selName) selName.textContent = name;
|
||||
}
|
||||
|
||||
function deselectWebdavFile() {
|
||||
_aiWebdavSelectedPath = '';
|
||||
_aiWebdavSelectedName = '';
|
||||
var selDiv = document.getElementById('lh-ai-webdav-selected');
|
||||
if (selDiv) selDiv.classList.add('hidden');
|
||||
var browser = document.getElementById('lh-ai-webdav-browser');
|
||||
var selDiv = document.getElementById('lh-ai-webdav-selected');
|
||||
if (selDiv) selDiv.style.display = 'none';
|
||||
if (browser) browser.style.display = '';
|
||||
}
|
||||
|
||||
// ── Generate ────────────────────────────────────────────────
|
||||
|
|
@ -713,9 +720,13 @@
|
|||
var fileInput = document.getElementById('lh-ai-file');
|
||||
if (!fileInput || !fileInput.files[0]) { showToast('Select a file to upload', 'error'); return; }
|
||||
formData.append('file', fileInput.files[0]);
|
||||
var uploadCtx = document.getElementById('lh-ai-upload-context');
|
||||
if (uploadCtx && uploadCtx.value.trim()) formData.append('topic', uploadCtx.value.trim());
|
||||
} else if (tabName === 'webdav') {
|
||||
if (!_aiWebdavSelectedPath) { showToast('Select a file from Nextcloud', 'error'); return; }
|
||||
formData.append('webdavPath', _aiWebdavSelectedPath);
|
||||
var webdavCtx = document.getElementById('lh-ai-webdav-context');
|
||||
if (webdavCtx && webdavCtx.value.trim()) formData.append('topic', webdavCtx.value.trim());
|
||||
}
|
||||
|
||||
var btn = document.getElementById('btn-lh-ai-generate');
|
||||
|
|
@ -784,21 +795,31 @@
|
|||
}
|
||||
}
|
||||
|
||||
function runAiRefineBody() {
|
||||
var instructions = prompt('How should the body be refined?\n(e.g., "Make it more concise", "Add a clinical case", "Focus on management")');
|
||||
if (!instructions || !instructions.trim()) return;
|
||||
function toggleRefineBar(forceShow) {
|
||||
var bar = document.getElementById('lh-ai-refine-bar');
|
||||
var input = document.getElementById('lh-ai-refine-input');
|
||||
if (!bar) return;
|
||||
var show = forceShow !== undefined ? forceShow : bar.style.display === 'none';
|
||||
bar.style.display = show ? 'flex' : 'none';
|
||||
if (show && input) { input.value = ''; input.focus(); }
|
||||
}
|
||||
|
||||
function submitRefineBody() {
|
||||
var input = document.getElementById('lh-ai-refine-input');
|
||||
var instructions = input ? input.value.trim() : '';
|
||||
if (!instructions) { showToast('Enter instructions for refinement', 'error'); return; }
|
||||
|
||||
var currentBody = _bodyEditor ? getTpHTML(_bodyEditor) : '';
|
||||
if (!currentBody) { showToast('No body content to refine', 'error'); return; }
|
||||
|
||||
var model = document.getElementById('lh-ai-model') ? document.getElementById('lh-ai-model').value : getSelectedModel();
|
||||
|
||||
toggleRefineBar(false);
|
||||
showLoading('Refining content…');
|
||||
|
||||
fetch('/api/admin/learning/ai-refine', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ content: currentBody, instructions: instructions.trim(), model: model })
|
||||
body: JSON.stringify({ content: currentBody, instructions: instructions, model: model })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
|
||||
document.getElementById('btn-nc-save-path').addEventListener('click', function() {
|
||||
var p = (document.getElementById('nc-webdav-path').value || '').trim();
|
||||
fetch('/api/admin/learning/webdav-path', {
|
||||
fetch('/api/user/webdav-path', {
|
||||
method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ path: p })
|
||||
}).then(function(r) { return r.json(); })
|
||||
.then(function(d) { d.success ? showToast('Path saved', 'success') : showToast(d.error || 'Failed', 'error'); })
|
||||
|
|
|
|||
16
server.js
16
server.js
|
|
@ -19,8 +19,8 @@ app.use(helmet({
|
|||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrcAttr: ["'unsafe-inline'"], // allow onclick/onchange attribute handlers
|
||||
scriptSrc: ["'self'"],
|
||||
scriptSrcAttr: ["'none'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com', 'https://cdnjs.cloudflare.com'],
|
||||
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https://cdnjs.cloudflare.com'],
|
||||
imgSrc: ["'self'", 'data:', 'blob:'],
|
||||
|
|
@ -156,6 +156,18 @@ app.use('/api', require('./src/routes/wellVisit'));
|
|||
app.use('/api', require('./src/routes/sickVisit'));
|
||||
app.use('/api/admin/learning', require('./src/routes/learningAI'));
|
||||
|
||||
// User-level preference: save WebDAV learning path (auth only, not moderator-only)
|
||||
(function() {
|
||||
var { authMiddleware } = require('./src/middleware/auth');
|
||||
var db = require('./src/db/database');
|
||||
app.post('/api/user/webdav-path', authMiddleware, async function(req, res) {
|
||||
try {
|
||||
await db.run('UPDATE users SET webdav_learning_path = ? WHERE id = ?', [req.body.path || null, req.user.id]);
|
||||
res.json({ success: true });
|
||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
})();
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
|
|
|
|||
Loading…
Reference in a new issue