feat(corpus): add save-to-corpus + private corpus search scope
- POST /api/save-to-corpus.php — saves tool output text to user's default CaveauAI corpus via ClientRagPipeline
- api/case/upload.php — dual-writes uploaded PDFs to CaveauAI client_documents (best-effort)
- assets/js/corpus-save.js — shared <dialog> handler for .js-save-corpus buttons on all tool pages
- includes/layout_footer.php — injects corpus-save.js + shared save dialog markup
- korrespond/deep-research/barnevernet/discrepancy JS — save-to-corpus buttons on output sections
- api/search.php + LegalTools::search() — corpus_scope param ('shared'|'private'|'both'), merges personal CaveauAI corpus with shared legal library when 'both'
- includes/tool_form.php + assets/js/tools.js — corpus scope radio toggle shown on search tab
- api/user-docs.php — add POST upload method for non-SSO authenticated users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -813,6 +813,21 @@
|
||||
els.results.appendChild(finalContainer.firstChild);
|
||||
}
|
||||
|
||||
// Save-to-corpus button
|
||||
const briefEl = els.results.querySelector('.dr-brief');
|
||||
if (briefEl) {
|
||||
briefEl.id = 'bvjBriefText';
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.type = 'button';
|
||||
saveBtn.className = 'js-save-corpus secondary-button';
|
||||
saveBtn.dataset.tool = 'barnevernet';
|
||||
saveBtn.dataset.contentId = 'bvjBriefText';
|
||||
saveBtn.dataset.suggestedTitle = 'BVJ analyse: ' + (document.getElementById('bvjQuestion')?.value?.slice(0, 80) ?? 'Svar');
|
||||
saveBtn.textContent = 'Save to corpus';
|
||||
saveBtn.style.marginTop = '12px';
|
||||
briefEl.insertAdjacentElement('afterend', saveBtn);
|
||||
}
|
||||
|
||||
// Bind source card clicks
|
||||
els.results.querySelectorAll('.dr-source-card[data-source-n]').forEach((node) => {
|
||||
node.addEventListener('click', (e) => {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* corpus-save.js — "Save to corpus" shared handler for all DBN tool pages.
|
||||
*
|
||||
* Buttons that trigger a save must have:
|
||||
* class="js-save-corpus"
|
||||
* data-content-id="<id of element containing the text to save>"
|
||||
* data-tool="<source_tool slug, e.g. korrespond>"
|
||||
* data-suggested-title="<pre-filled title string>" (optional)
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const dlg = document.getElementById('save-corpus-dialog');
|
||||
const form = document.getElementById('save-corpus-form');
|
||||
const titleIn = document.getElementById('save-corpus-title');
|
||||
const tagsIn = document.getElementById('save-corpus-tags');
|
||||
const cancelBtn = document.getElementById('save-corpus-cancel');
|
||||
|
||||
if (!dlg || !form) return; // dialog not present (e.g. not logged in)
|
||||
|
||||
cancelBtn?.addEventListener('click', () => dlg.close());
|
||||
|
||||
let _pendingBtn = null;
|
||||
let _pendingContent = '';
|
||||
let _pendingTool = '';
|
||||
|
||||
// Delegated click — catches buttons added dynamically by tool JS
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.js-save-corpus');
|
||||
if (!btn) return;
|
||||
|
||||
const contentId = btn.dataset.contentId;
|
||||
const el = contentId ? document.getElementById(contentId) : null;
|
||||
const content = (el ? (el.value ?? el.textContent) : '').trim();
|
||||
|
||||
if (!content || content.length < 30) {
|
||||
btn.textContent = 'Nothing to save';
|
||||
setTimeout(() => { btn.textContent = 'Save to corpus'; }, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingBtn = btn;
|
||||
_pendingContent = content;
|
||||
_pendingTool = btn.dataset.tool ?? '';
|
||||
|
||||
titleIn.value = btn.dataset.suggestedTitle ?? '';
|
||||
tagsIn.value = '';
|
||||
dlg.showModal();
|
||||
titleIn.focus();
|
||||
titleIn.select();
|
||||
});
|
||||
|
||||
// Form submit inside dialog
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
dlg.close();
|
||||
|
||||
const btn = _pendingBtn;
|
||||
const content = _pendingContent;
|
||||
const title = titleIn.value.trim();
|
||||
const tags = tagsIn.value.trim();
|
||||
const tool = _pendingTool;
|
||||
|
||||
if (!title || !content) return;
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Saving…';
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('api/save-to-corpus.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, content, source_tool: tool, tags }),
|
||||
});
|
||||
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
|
||||
if (resp.ok && data.ok) {
|
||||
if (btn) {
|
||||
btn.textContent = '✓ Saved to corpus';
|
||||
btn.classList.add('js-save-corpus--saved');
|
||||
}
|
||||
} else {
|
||||
const msg = data.error ?? `Error ${resp.status}`;
|
||||
if (btn) {
|
||||
btn.textContent = 'Save failed';
|
||||
btn.disabled = false;
|
||||
btn.title = msg;
|
||||
}
|
||||
console.error('[corpus-save] Save failed:', msg);
|
||||
}
|
||||
} catch (err) {
|
||||
if (btn) {
|
||||
btn.textContent = 'Network error';
|
||||
btn.disabled = false;
|
||||
}
|
||||
console.error('[corpus-save] Network error:', err);
|
||||
}
|
||||
|
||||
_pendingBtn = null;
|
||||
_pendingContent = '';
|
||||
});
|
||||
}());
|
||||
@@ -563,6 +563,21 @@
|
||||
${nextHtml}
|
||||
`;
|
||||
|
||||
// Save-to-corpus button (inject after brief block)
|
||||
const briefEl = els.results.querySelector('.dr-brief');
|
||||
if (briefEl) {
|
||||
briefEl.id = 'drBriefText';
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.type = 'button';
|
||||
saveBtn.className = 'js-save-corpus secondary-button';
|
||||
saveBtn.dataset.tool = 'deep-research';
|
||||
saveBtn.dataset.contentId = 'drBriefText';
|
||||
saveBtn.dataset.suggestedTitle = 'Research: ' + (document.getElementById('drQuery')?.value?.slice(0, 80) ?? 'Report');
|
||||
saveBtn.textContent = 'Save to corpus';
|
||||
saveBtn.style.marginTop = '12px';
|
||||
briefEl.insertAdjacentElement('afterend', saveBtn);
|
||||
}
|
||||
|
||||
// Bind source-card click handlers (open modal) — but ignore clicks on inner <a>
|
||||
els.results.querySelectorAll('.dr-source-card[data-source-n]').forEach((node) => {
|
||||
node.addEventListener('click', (e) => {
|
||||
|
||||
@@ -551,6 +551,17 @@
|
||||
els.results.appendChild(finalContainer.firstChild);
|
||||
}
|
||||
|
||||
// Save-to-corpus button (appended after final results)
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.type = 'button';
|
||||
saveBtn.className = 'js-save-corpus secondary-button';
|
||||
saveBtn.dataset.tool = 'discrepancy';
|
||||
saveBtn.dataset.contentId = 'dcResults';
|
||||
saveBtn.dataset.suggestedTitle = 'Discrepancy report';
|
||||
saveBtn.textContent = 'Save to corpus';
|
||||
saveBtn.style.marginTop = '16px';
|
||||
els.results.appendChild(saveBtn);
|
||||
|
||||
// Bind tabs
|
||||
els.results.querySelectorAll('.dc-tab').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
|
||||
@@ -543,6 +543,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<pre class="korr-draft-body" id="korrDraftNo">${esc(draftNo)}</pre>
|
||||
<button type="button" class="js-save-corpus secondary-button"
|
||||
data-tool="korrespond"
|
||||
data-content-id="korrDraftNo"
|
||||
data-suggested-title="${esc((data.output_type || 'Brev') + ' — ' + (data.recipient_body || ''))}">
|
||||
Save to corpus
|
||||
</button>
|
||||
</div>
|
||||
${isSameLang ? '' : `
|
||||
<div class="korr-draft-col">
|
||||
@@ -554,6 +560,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<pre class="korr-draft-body" id="korrDraftUser">${esc(draftUser)}</pre>
|
||||
<button type="button" class="js-save-corpus secondary-button"
|
||||
data-tool="korrespond"
|
||||
data-content-id="korrDraftUser"
|
||||
data-suggested-title="${esc((data.output_type || 'Brev') + ' — ' + (data.recipient_body || '') + ' (translation)')}">
|
||||
Save to corpus
|
||||
</button>
|
||||
</div>`}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -910,6 +910,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
uploadFileList: document.querySelector('#uploadFileList'),
|
||||
uploadClear: document.querySelector('#uploadClear'),
|
||||
aliasSection: document.querySelector('#aliasSection'),
|
||||
corpusScopeControl: document.querySelector('#corpusScopeControl'),
|
||||
addAliasRow: document.querySelector('#addAliasRow'),
|
||||
aliasRows: document.querySelector('#aliasRows'),
|
||||
audioZone: document.querySelector('#audioZone'),
|
||||
@@ -1013,6 +1014,7 @@ function setTool(toolName) {
|
||||
els.input.placeholder = tool.placeholder;
|
||||
}
|
||||
els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage);
|
||||
els.corpusScopeControl?.classList.toggle('is-hidden', toolName !== 'search');
|
||||
els.redactionControl.classList.toggle('is-hidden', toolName !== 'redact');
|
||||
els.uploadZone.classList.toggle('is-hidden', toolName !== 'redact' && toolName !== 'timeline');
|
||||
els.aliasSection.classList.toggle('is-hidden', toolName !== 'redact');
|
||||
@@ -1080,6 +1082,7 @@ async function runTool(event) {
|
||||
}
|
||||
if (state.activeTool === 'search') {
|
||||
payload.limit = 7;
|
||||
payload.corpus_scope = currentCorpusScope();
|
||||
}
|
||||
if (state.activeTool === 'redact') {
|
||||
lastOriginalText = text;
|
||||
@@ -1329,6 +1332,10 @@ function currentLanguage() {
|
||||
return document.querySelector('input[name="language"]:checked')?.value || 'en';
|
||||
}
|
||||
|
||||
function currentCorpusScope() {
|
||||
return document.querySelector('input[name="corpusScope"]:checked')?.value || 'both';
|
||||
}
|
||||
|
||||
function currentRedactionMode() {
|
||||
return document.querySelector('input[name="redactionMode"]:checked')?.value || 'standard';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user