Files
daveadmin 56cd87dd7b redact: UX overhaul — engine simplification, credits, spinner, save-to-docs, badges
- Remove GPU/regex engine options; keep only azure_mini (1 credit) and azure_full (2 credits)
- Variable credit cost: engine-aware pre-check and charge in api/redact.php; PricingCatalog base = 1
- Fix ATTORNEY not preserved when keepOfficials=true: add to LLM prompt, generic-tag, pseudonym regexes
- Replace Azure credits hint with per-engine credit cost text (all 4 languages)
- Single-file upload only (was: up to 5); simplify status messages
- Clear previous redaction output and show pulsing spinner when a new run starts
- Add "Save to My Docs" button in redact output panel (corpus-save.js path)
- corpus-save.js: capture source_doc_ids from button dataset, pass in POST payload
- api/save-to-corpus.php: accept source_doc_ids, store first as source_url=corpus-doc:{id}
- doc-picker.js: show "✂ Redacted" badge for documents saved from the redact tool
- CSS: .redact-working spinner, doc-item__badge--redact pill styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:18:51 +02:00

169 lines
5.7 KiB
JavaScript

/**
* corpus-save.js — Shared "Save to corpus" handler.
*
* Two trigger paths:
*
* 1. Tool pages — buttons with class="js-save-corpus":
* data-content-id="<id of element with text>"
* data-tool="<source_tool slug, e.g. korrespond>"
* data-suggested-title="<pre-filled title>" (optional)
*
* 2. Dashboard chat — page code calls:
* dialog.dataset.pendingContent = "...";
* dialog.dataset.pendingTool = "dashboard-chat"; (optional)
* dialog.showModal();
* Title and tags are populated by the caller before showModal().
*
* Endpoint resolution: if window.DBN_DASHBOARD is present (dashboard pages),
* POST to /api/dashboard/save-from-tool.php; otherwise fall back to the
* legacy /api/save-to-corpus.php so existing tools keep working.
*/
(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;
cancelBtn?.addEventListener('click', () => dlg.close());
let _pendingBtn = null;
function endpoint() {
return window.DBN_DASHBOARD
? '/api/dashboard/save-from-tool.php'
: '/api/save-to-corpus.php';
}
function bodyFor(kind, payload) {
const sourceDocIds = (payload.sourceDocIds || '').split(',').map(s => s.trim()).filter(Boolean);
if (window.DBN_DASHBOARD) {
return JSON.stringify({
title: payload.title,
content: payload.content,
source_tool: payload.tool || 'dashboard-save',
tags: payload.tags,
kind,
...(sourceDocIds.length ? { source_doc_ids: sourceDocIds } : {}),
});
}
return JSON.stringify({
title: payload.title,
content: payload.content,
source_tool: payload.tool || '',
tags: payload.tags,
...(sourceDocIds.length ? { source_doc_ids: sourceDocIds } : {}),
});
}
// ── Path 1: legacy tool buttons (.js-save-corpus) ─────────────────────
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;
dlg.dataset.pendingContent = content;
dlg.dataset.pendingTool = btn.dataset.tool || '';
dlg.dataset.pendingKind = 'tool_output';
dlg.dataset.pendingSourceDocIds = btn.dataset.sourceDocIds || '';
titleIn.value = btn.dataset.suggestedTitle || '';
tagsIn.value = '';
dlg.showModal();
titleIn.focus();
titleIn.select();
});
// ── Submit dialog (both paths) ────────────────────────────────────────
form.addEventListener('submit', async (e) => {
e.preventDefault();
dlg.close();
const btn = _pendingBtn;
const content = dlg.dataset.pendingContent || '';
const tool = dlg.dataset.pendingTool || '';
const kind = dlg.dataset.pendingKind || 'tool_output';
const sourceDocIds = dlg.dataset.pendingSourceDocIds || '';
const title = titleIn.value.trim();
const tags = tagsIn.value.trim();
if (!title || !content) return;
if (btn) {
btn.disabled = true;
btn.textContent = 'Saving…';
}
try {
const resp = await fetch(endpoint(), {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: bodyFor(kind, { title, content, tool, tags, sourceDocIds }),
});
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 {
// Path 2 (no button): show a fleeting toast
showToast('Lagret i korpus — ' + (data.chunks || 0) + ' passasjer');
}
} else {
const msg = (data.error && data.error.message) || data.error || ('Error ' + resp.status);
if (btn) {
btn.textContent = 'Save failed';
btn.disabled = false;
btn.title = msg;
} else {
showToast('Lagring feilet: ' + msg, true);
}
console.error('[corpus-save] Save failed:', msg);
}
} catch (err) {
if (btn) {
btn.textContent = 'Network error';
btn.disabled = false;
} else {
showToast('Nettverksfeil', true);
}
console.error('[corpus-save] Network error:', err);
}
_pendingBtn = null;
delete dlg.dataset.pendingContent;
delete dlg.dataset.pendingTool;
delete dlg.dataset.pendingKind;
delete dlg.dataset.pendingSourceDocIds;
});
function showToast(msg, isError) {
const t = document.createElement('div');
t.textContent = msg;
t.style.cssText =
'position:fixed;bottom:1.5rem;left:50%;transform:translateX(-50%);' +
'padding:0.65rem 1rem;border-radius:10px;font:inherit;font-size:0.9rem;' +
'z-index:99999;color:#fff;background:' + (isError ? '#ba0c2f' : '#00205b') + ';' +
'box-shadow:0 8px 24px rgba(0,0,0,0.25);';
document.body.appendChild(t);
setTimeout(() => t.remove(), 3500);
}
}());