Add document upload to Redact tool
api/extract.php — new endpoint accepting .pdf/.docx/.txt up to 4 MB; pdftotext for PDFs, ZipArchive+DOMXPath for DOCX, mb_convert_encoding for TXT; truncates to 32 000 chars to stay within redact limit. index.php — drop/browse upload zone above the textarea, visible only in Redact mode. tools.js — setupUpload(), handleFileUpload(), resetUpload(); drag-and-drop and file picker both call the extract endpoint then populate the textarea. tools.css — upload zone, drag-over, file-info, clear button styles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
traceList: document.querySelector('#traceList'),
|
||||
healthButton: document.querySelector('#healthButton'),
|
||||
healthPill: document.querySelector('#healthPill'),
|
||||
uploadZone: document.querySelector('#uploadZone'),
|
||||
uploadInput: document.querySelector('#uploadInput'),
|
||||
uploadPrompt: document.querySelector('#uploadPrompt'),
|
||||
uploadFileInfo: document.querySelector('#uploadFileInfo'),
|
||||
uploadFileName: document.querySelector('#uploadFileName'),
|
||||
uploadClear: document.querySelector('#uploadClear'),
|
||||
});
|
||||
|
||||
els.tabs.forEach((button) => {
|
||||
@@ -88,6 +94,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
els.form.addEventListener('submit', runTool);
|
||||
els.passcodeForm.addEventListener('submit', submitPasscode);
|
||||
els.healthButton.addEventListener('click', checkHealth);
|
||||
setupUpload();
|
||||
setTool(state.activeTool);
|
||||
|
||||
if (state.authenticated) {
|
||||
@@ -114,6 +121,8 @@ function setTool(toolName) {
|
||||
els.input.placeholder = tool.placeholder;
|
||||
els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage);
|
||||
els.redactionControl.classList.toggle('is-hidden', toolName !== 'redact');
|
||||
els.uploadZone.classList.toggle('is-hidden', toolName !== 'redact');
|
||||
resetUpload();
|
||||
els.status.textContent = '';
|
||||
renderTrace([]);
|
||||
}
|
||||
@@ -186,6 +195,92 @@ async function runTool(event) {
|
||||
}
|
||||
}
|
||||
|
||||
function resetUpload() {
|
||||
if (!els.uploadInput) return;
|
||||
els.uploadInput.value = '';
|
||||
els.uploadPrompt.classList.remove('is-hidden');
|
||||
els.uploadFileInfo.classList.add('is-hidden');
|
||||
els.uploadFileName.textContent = '';
|
||||
els.uploadZone.classList.remove('is-drag-over');
|
||||
}
|
||||
|
||||
function setupUpload() {
|
||||
els.uploadZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
els.uploadZone.classList.add('is-drag-over');
|
||||
});
|
||||
|
||||
els.uploadZone.addEventListener('dragleave', (e) => {
|
||||
if (!els.uploadZone.contains(e.relatedTarget)) {
|
||||
els.uploadZone.classList.remove('is-drag-over');
|
||||
}
|
||||
});
|
||||
|
||||
els.uploadZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
els.uploadZone.classList.remove('is-drag-over');
|
||||
const file = e.dataTransfer?.files?.[0];
|
||||
if (file) handleFileUpload(file);
|
||||
});
|
||||
|
||||
els.uploadZone.addEventListener('click', (e) => {
|
||||
if (e.target === els.uploadClear || els.uploadClear?.contains(e.target)) return;
|
||||
if (e.target.tagName === 'LABEL') return;
|
||||
els.uploadInput.click();
|
||||
});
|
||||
|
||||
els.uploadInput.addEventListener('change', () => {
|
||||
const file = els.uploadInput.files?.[0];
|
||||
if (file) handleFileUpload(file);
|
||||
});
|
||||
|
||||
els.uploadClear.addEventListener('click', () => {
|
||||
resetUpload();
|
||||
els.status.textContent = '';
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFileUpload(file) {
|
||||
const allowed = ['pdf', 'docx', 'txt'];
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
if (!allowed.includes(ext)) {
|
||||
els.status.textContent = 'Unsupported file type. Use .pdf, .docx, or .txt.';
|
||||
return;
|
||||
}
|
||||
|
||||
els.status.textContent = `Extracting ${file.name}…`;
|
||||
setBusy(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const resp = await fetch('api/extract.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: formData,
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
|
||||
if (!resp.ok || !data.ok) {
|
||||
throw new Error(data.error?.message || `Extraction failed (HTTP ${resp.status}).`);
|
||||
}
|
||||
|
||||
els.input.value = data.text;
|
||||
els.uploadFileName.textContent = file.name;
|
||||
els.uploadPrompt.classList.add('is-hidden');
|
||||
els.uploadFileInfo.classList.remove('is-hidden');
|
||||
|
||||
const note = data.truncated ? ' (truncated to 32 000 chars)' : '';
|
||||
els.status.textContent = `Extracted ${data.chars.toLocaleString()} chars from ${file.name}${note}.`;
|
||||
} catch (err) {
|
||||
els.status.textContent = err.message;
|
||||
resetUpload();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
els.healthPill.textContent = 'Checking...';
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user