Spaces:
Running on Zero
Running on Zero
File size: 18,956 Bytes
9da9b01 0b08f72 2a0b720 9da9b01 0b08f72 bc9652b 0b08f72 9da9b01 0b08f72 9da9b01 0b08f72 9da9b01 2a0b720 0b08f72 2a0b720 0b08f72 9da9b01 0b08f72 9da9b01 0b08f72 9da9b01 0b08f72 9da9b01 5785458 9da9b01 0b08f72 ef2e115 9da9b01 0b08f72 9da9b01 0b08f72 9da9b01 0b08f72 9da9b01 5785458 9da9b01 5785458 9da9b01 0b08f72 9da9b01 0b08f72 9da9b01 0b08f72 9da9b01 0b08f72 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 | // 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);
})();
|