humanizer / page.js
jialinyyzz's picture
Create fact: one-line how-to under the draft whenever it has text
5785458 verified
Raw History Blame Contribute Delete
19 kB
// humanizer Space: language switch, theme toggle, sample drafts, word counter, copy buttons,
// Create fact (keep selected passages word for word) and .docx / PDF import.
// Runs from <head>; everything is delegated from document, so Gradio re-renders don't matter.
(function () {
var MAX_WORDS = 700, MAX_CJK = 1200, PARTS = 3; // keep in step with page.py
var MAX_FACTS = 50; // keep in step with facts.py
var LONG_DRAFT_TOKENS = 1000; // the app's long-draft note (app/web/js/text.js)
var root = document.documentElement;
var KEY = 'hz-space-lang';
function store(k, v) { try { if (v === undefined) return localStorage.getItem(k); localStorage.setItem(k, v); } catch (e) { return null; } }
var lang = null;
try { var q = new URLSearchParams(location.search).get('lang'); if (q === 'zh' || q === 'en') lang = q; } catch (e) {}
if (!lang) lang = store(KEY);
if (lang !== 'zh' && lang !== 'en') lang = 'en'; // English unless ?lang=zh or the visitor picked 中文 before
root.dataset.lang = lang;
root.lang = lang === 'zh' ? 'zh-CN' : 'en';
var PH = { en: 'Paste the AI-written draft here…', zh: '把 AI 写的草稿贴在这里……' };
function $(s, el) { return (el || document).querySelector(s); }
function draftArea() { return $('#draft textarea'); }
function draftValue() { var ta = draftArea(); return ta ? ta.value : ''; }
function isZh() { return root.dataset.lang === 'zh'; }
function busy() { return root.classList.contains('hz-busy'); }
// ── word counter (same units as hz: English words + Chinese characters) ──
var CJK = /[぀-ヿ㐀-䶿一-鿿豈-﫿가-힯]/g;
function count(s) {
var cjk = (s.match(CJK) || []).length;
var latin = s.replace(CJK, ' ').split(/\s+/).filter(function (t) { return /[\p{L}\p{N}]/u.test(t); }).length;
return { cjk: cjk, latin: latin };
}
function updateCount() {
var ta = draftArea(), el = $('#draft-count');
if (!ta || !el) return;
var c = count(ta.value || ''), zh = c.cjk > c.latin;
var n = c.cjk + c.latin, size = c.latin + c.cjk * MAX_WORDS / MAX_CJK;
var unitEn = zh ? 'chars' : 'words', unitZh = '字';
if (!n) zh = isZh();
var lim = (zh ? MAX_CJK * PARTS : MAX_WORDS * PARTS).toLocaleString('en-US');
el.innerHTML = '<b>' + n.toLocaleString('en-US') + '</b> <span class="l-en">' + unitEn + '</span><span class="l-zh">' + unitZh +
'</span> <span class="dim">/ ' + lim + '</span>';
el.classList.toggle('over', size > MAX_WORDS * PARTS);
ta.classList.toggle('cjk', zh);
// long documents may drift on facts (rough token estimate, as in the app: 1.5 per English word, 0.95 per character)
var note = $('#draft-long');
if (note) note.hidden = Math.round(c.latin * 1.5 + c.cjk * 0.95) < LONG_DRAFT_TOKENS;
}
function applyLang() {
var ta = draftArea();
if (ta) ta.placeholder = PH[root.dataset.lang] || PH.en;
document.querySelectorAll('[data-title-en]').forEach(function (el) {
el.title = isZh() ? el.dataset.titleZh : el.dataset.titleEn;
});
renderFacts();
}
function setLang(l) {
root.dataset.lang = l;
root.lang = l === 'zh' ? 'zh-CN' : 'en';
store(KEY, l);
applyLang();
}
function setDraft(text) {
var ta = draftArea();
if (!ta) return;
var setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
setter.call(ta, text);
ta.dispatchEvent(new Event('input', { bubbles: true }));
facts = [];
renderFacts();
updateCount();
}
function copyText(text, btn) {
function done() { if (!btn) return; btn.classList.add('copied'); setTimeout(function () { btn.classList.remove('copied'); }, 1600); }
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallback(text); done(); });
} else { fallback(text); done(); }
}
function fallback(text) {
var t = document.createElement('textarea');
t.value = text; t.style.position = 'fixed'; t.style.opacity = '0';
document.body.appendChild(t); t.select();
try { document.execCommand('copy'); } catch (e) {}
t.remove();
}
// ── toasts (the ones from the server land in #hz-toast; these are made here) ──
function toast(en, zh, kind) {
var host = $('.gradio-container') || document.body; // the stylesheet is scoped to .gradio-container
document.querySelectorAll('.toast.js').forEach(function (t) { t.remove(); });
var old = $('#hz-toast .toast'); if (old) old.classList.add('gone');
var t = document.createElement('div');
t.className = 'toast js';
t.dataset.kind = kind || 'ok';
t.setAttribute('role', 'status');
var a = document.createElement('span'); a.className = 'l-en'; a.textContent = en;
var b = document.createElement('span'); b.className = 'l-zh'; b.lang = 'zh-CN'; b.textContent = zh;
t.appendChild(a); t.appendChild(b);
t.addEventListener('animationend', function () { t.remove(); });
host.appendChild(t);
}
// ── Create fact: selected draft text comes back word for word (rules in facts.py / the app's facts.js) ──
var facts = [];
function pruneFacts() {
var v = draftValue(), seen = {};
facts = facts.filter(function (f) {
if (typeof f !== 'string' || !f.trim() || v.indexOf(f) < 0 || seen[f]) return false;
seen[f] = true; return true;
}).slice(0, MAX_FACTS);
}
function renderFacts(fresh) {
var panel = $('#facts-panel'), list = $('#facts-list'), cnt = $('#facts-count');
if (!panel || !list) return;
// shown whenever the draft has text: with no facts yet it explains how to add one
panel.hidden = !facts.length && !draftValue().trim();
var hint = $('#facts-hint');
if (hint) hint.hidden = facts.length > 0;
if (cnt) cnt.textContent = facts.length > 1 ? String(facts.length) : '';
list.textContent = '';
facts.forEach(function (f, i) {
var li = document.createElement('li');
li.className = 'fact-chip' + (f === fresh ? ' new' : '');
li.title = f;
var sp = document.createElement('span');
sp.className = 'fact-text';
sp.textContent = f.replace(/\s+/g, ' ');
var x = document.createElement('button');
x.type = 'button';
x.className = 'fact-x';
x.dataset.i = String(i);
x.innerHTML = '<svg aria-hidden="true"><use href="#i-close"/></svg>';
x.setAttribute('aria-label', (isZh() ? '不再保留:' : 'Stop keeping: ') + f);
x.title = isZh() ? '移除' : 'Remove';
li.appendChild(sp); li.appendChild(x);
list.appendChild(li);
});
}
function selectedFact() {
var ta = draftArea();
if (!ta || busy()) return '';
return ta.value.slice(ta.selectionStart, ta.selectionEnd).trim();
}
// Where the selection sits inside the textarea: a hidden mirror with the same layout (textareas give no coordinates).
var MIRROR = ['boxSizing', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle',
'fontVariationSettings', 'letterSpacing', 'lineHeight', 'textTransform', 'wordSpacing', 'textIndent', 'tabSize'];
function caretXY(ta, pos) {
var cs = getComputedStyle(ta), m = document.createElement('div');
MIRROR.forEach(function (p) { m.style[p] = cs[p]; });
Object.assign(m.style, { position: 'absolute', visibility: 'hidden', top: '0', left: '-9999px', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', width: ta.clientWidth + 'px', border: '0' });
m.textContent = ta.value.slice(0, pos);
var mark = document.createElement('span');
mark.textContent = '​';
m.appendChild(mark);
document.body.appendChild(m);
var r = { x: mark.offsetLeft, y: mark.offsetTop - ta.scrollTop, h: parseFloat(cs.lineHeight) || 28 };
m.remove();
return r;
}
function placePop() {
var ta = draftArea(), pop = $('#btn-create-fact'), layer = pop && pop.closest('.draft-overlays');
if (!ta || !pop || !layer) return;
var tr = ta.getBoundingClientRect(), sr = layer.getBoundingClientRect();
var ox = tr.left - sr.left, oy = tr.top - sr.top;
var w = pop.offsetWidth, h = pop.offsetHeight, W = ta.clientWidth, H = ta.clientHeight;
var x = W - w - 16, y = H - h - 12; // a very long draft: no coordinates, bottom right
if (ta.value.length < 60000) {
var a = caretXY(ta, ta.selectionStart), b = caretXY(ta, ta.selectionEnd);
x = (a.y === b.y ? (a.x + b.x) / 2 : a.x + 60) - w / 2;
y = a.y - h - 6; // above the first line of the selection; too high -> below the last line
if (y < 6) y = b.y + b.h + 4;
}
pop.style.left = Math.round(ox + Math.min(Math.max(x, 10), W - w - 10)) + 'px';
pop.style.top = Math.round(oy + Math.min(Math.max(y, 6), H - h - 6)) + 'px';
}
function updatePop() {
var pop = $('#btn-create-fact'), ta = draftArea();
if (!pop || !ta) return;
var sel = selectedFact();
var focused = document.activeElement === ta || document.activeElement === pop;
var show = !!sel && focused;
pop.hidden = !show;
if (!show) return;
var kept = facts.indexOf(sel) >= 0;
pop.disabled = kept;
pop.classList.toggle('kept', kept);
placePop();
}
function createFact() {
var f = selectedFact();
if (!f || facts.indexOf(f) >= 0) return;
if (facts.length >= MAX_FACTS) { toast('You can keep up to 50 facts.', '最多保留 50 段文字。', 'err'); return; }
facts.push(f);
renderFacts(f);
var ta = draftArea();
ta.focus({ preventScroll: true });
ta.setSelectionRange(ta.selectionEnd, ta.selectionEnd);
updatePop();
toast('Fact created: this text will be kept word for word.', '好的,这段文字改写时一字不改。', 'ok');
}
// ── import .docx / .pdf: checked here, handed to the hidden Gradio upload, read on the server ──
var importing = false, beforeImport = null, importTimer = 0;
function setImporting(on) {
importing = on;
var b = $('#btn-import');
if (!b) return;
b.classList.toggle('busy', on);
b.disabled = on;
b.setAttribute('aria-busy', String(on));
var u = b.querySelector('use');
if (u) u.setAttribute('href', on ? '#i-spin' : '#i-import');
}
function importFile(file) {
if (!file || importing || busy()) return;
var fi = $('#doc-file'), max = (fi && +fi.dataset.max) || 10485760, mb = Math.round(max / 1048576);
if (!/\.(docx|pdf)$/i.test(file.name)) {
toast('Only .docx and .pdf files are supported. Save old .doc files as .docx first.', '只支持 .docx 和 .pdf。旧版 .doc 请先在 Word 里另存为 .docx。', 'err');
return;
}
if (file.size > max) {
toast('This file is over ' + mb + ' MB, the limit on this page. The app takes files up to 20 MB.', '文件超过 ' + mb + ' MB,本页读不了。App 能读 20 MB 以内的文件。', 'err');
return;
}
var cur = draftValue();
if (cur.trim() && !confirm(isZh() ? '用「' + file.name + '」里的文字替换当前草稿?' : 'Replace the current draft with the text from “' + file.name + '”?')) return;
var input = $('#hz-upload input[type=file]') || $('input[type=file][data-testid$="-upload-button"]');
if (!input) { toast('Couldn’t read this file. Reload the page and try again.', '读不了这个文件,请刷新页面再试。', 'err'); return; }
var dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
beforeImport = cur;
setImporting(true);
clearTimeout(importTimer);
importTimer = setTimeout(function () { setImporting(false); beforeImport = null; }, 120000); // never stay stuck
input.dispatchEvent(new Event('change', { bubbles: true }));
}
function afterImport() {
clearTimeout(importTimer);
document.querySelectorAll('.toast.js').forEach(function (t) { t.remove(); });
var before = beforeImport;
beforeImport = null;
setImporting(false);
// Gradio writes the new text into the textarea a moment after the event ends
setTimeout(function () {
if (before !== null && draftValue() !== before) { // replaced: the old kept passages go, as in the app
facts = [];
renderFacts();
var ta = draftArea();
if (ta) { ta.scrollTop = 0; ta.setSelectionRange(0, 0); }
}
updateCount();
}, 150);
}
// drop a file on the draft sheet; dropping it anywhere else must not navigate away from the page
var dragDepth = 0;
function sheetEl() { var ta = draftArea(); return ta && ta.closest('.draft-sheet'); }
function hasFiles(e) { return Array.prototype.indexOf.call((e.dataTransfer && e.dataTransfer.types) || [], 'Files') >= 0; }
function onSheet(e) { var s = sheetEl(); return !!(s && e.target && e.target.nodeType === 1 && s.contains(e.target)); }
function hideDrop() { dragDepth = 0; var z = $('#dropzone'); if (z) z.hidden = true; }
document.addEventListener('dragenter', function (e) {
if (!hasFiles(e) || !onSheet(e) || busy() || importing) return;
e.preventDefault();
dragDepth += 1;
var z = $('#dropzone'); if (z) z.hidden = false;
});
document.addEventListener('dragover', function (e) {
if (!hasFiles(e)) return;
e.preventDefault();
e.dataTransfer.dropEffect = onSheet(e) && !busy() && !importing ? 'copy' : 'none';
});
document.addEventListener('dragleave', function (e) {
if (hasFiles(e) && onSheet(e) && --dragDepth <= 0) hideDrop();
});
document.addEventListener('drop', function (e) {
if (!hasFiles(e)) return;
e.preventDefault();
var inside = onSheet(e);
hideDrop();
if (inside && e.dataTransfer.files && e.dataTransfer.files[0]) importFile(e.dataTransfer.files[0]);
});
window.addEventListener('dragend', hideDrop);
window.HZ = {
factsJSON: function () { pruneFacts(); return JSON.stringify(facts); },
afterImport: afterImport,
};
document.addEventListener('click', function (e) {
var a = e.target.closest('a[href^="#"]');
if (a && a.getAttribute('href').length > 1) {
// On huggingface.co the Space sits in an auto-height iframe, so a plain #hash jump scrolls nothing.
// scrollIntoView also scrolls the parent page.
var dest = document.getElementById(a.getAttribute('href').slice(1));
if (dest) { e.preventDefault(); dest.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
return;
}
if (e.target.closest('#btn-create-fact')) { if (!busy()) createFact(); return; }
var x = e.target.closest('.fact-x');
if (x) {
if (busy()) return;
facts.splice(+x.dataset.i, 1);
renderFacts();
updatePop();
return;
}
var t = e.target.closest('[data-setlang],[data-act],[data-sample]');
if (!t) return;
if (t.dataset.setlang) { setLang(t.dataset.setlang); return; }
if (t.dataset.sample) {
var s = (window.HZ_SAMPLES || {})[t.dataset.sample];
if (s) {
setDraft(s);
if (t.closest('#hz-body')) { var ed = $('#editor'); if (ed) ed.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
}
return;
}
switch (t.dataset.act) {
case 'theme': document.body.classList.toggle('dark'); break;
case 'clear': setDraft(''); var ta = draftArea(); if (ta) ta.focus(); break;
case 'diff': root.dataset.diff = root.dataset.diff === 'off' ? 'on' : 'off'; break;
case 'import': { var fi = $('#doc-file'); if (fi && !importing && !busy()) fi.click(); break; }
case 'copy-out': {
var pre = t.closest('.out-wrap') && t.closest('.out-wrap').querySelector('pre.plain');
if (pre) copyText(pre.textContent, t);
break;
}
case 'copy-code': {
var code = t.closest('.code') && t.closest('.code').querySelector('code');
if (code) copyText(code.textContent, t);
break;
}
}
});
document.addEventListener('change', function (e) {
if (e.target && e.target.id === 'doc-file') {
var f = e.target.files && e.target.files[0];
e.target.value = '';
if (f) importFile(f);
}
});
// keep the selection while the Create fact button is pressed
document.addEventListener('mousedown', function (e) { if (e.target.closest && e.target.closest('#btn-create-fact')) e.preventDefault(); });
document.addEventListener('input', function (e) {
if (e.target && e.target.closest && e.target.closest('#draft')) {
var n = facts.length, shown = !$('#facts-panel').hidden;
pruneFacts();
if (facts.length !== n || shown !== !!draftValue().trim()) renderFacts();
updateCount();
updatePop();
}
});
document.addEventListener('selectionchange', updatePop);
['mouseup', 'keyup'].forEach(function (ev) {
document.addEventListener(ev, function (e) { if (e.target && e.target.closest && e.target.closest('#draft')) updatePop(); });
});
document.addEventListener('scroll', function (e) {
var pop = $('#btn-create-fact');
if (pop && !pop.hidden && e.target === draftArea()) placePop();
}, true);
document.addEventListener('focusout', function (e) {
var pop = $('#btn-create-fact');
if (pop && e.target === draftArea() && e.relatedTarget !== pop) pop.hidden = true;
});
window.addEventListener('resize', function () { var pop = $('#btn-create-fact'); if (pop && !pop.hidden) placePop(); });
document.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && e.target.closest && e.target.closest('#draft')) {
e.preventDefault();
var go = $('#go');
if (go) go.click();
}
});
// On a direct *.hf.space visit Hugging Face pins its own badge (#huggingface-space-header) over the
// top-right corner; move the bar below it so the language switch stays reachable.
[500, 1500, 3000, 6000].forEach(function (ms) {
setTimeout(function () {
if (document.getElementById('huggingface-space-header')) root.classList.add('hz-hfbadge');
}, ms);
});
// Gradio renders after this script runs: wait for the textarea once, then set placeholder and counter,
// and move the layer with the Create fact button and the drop overlay onto the draft sheet.
var tries = 0;
var iv = setInterval(function () {
tries += 1;
var ta = draftArea();
if (ta && document.getElementById('ex-4') && $('#btn-create-fact')) {
var sheet = ta.closest('.draft-sheet'), layer = $('.draft-overlays');
if (sheet && layer) { sheet.appendChild(layer); layer.hidden = false; }
applyLang(); updateCount();
if (root.dataset.lang === 'zh') document.getElementById('ex-4').checked = true; // open on a Chinese example
var k = $('.go-kbd');
if (k && !/Mac|iPhone|iPad/.test(navigator.platform || '')) k.textContent = 'Ctrl ↵';
clearInterval(iv);
} else if (tries > 300) clearInterval(iv);
}, 100);
})();