import { Api } from './Api'; import { Mention } from './Mention'; import { c } from './lib/Log'; import { Utils } from './lib/Utils'; import { VanillaTilt } from './vanilla-tilt'; import { ShareScreenshotModal, SavePublishModal } from './modals'; /// We experimented with a couple of different build systems /// to integrate Quill (for instance module-then-postprocessing /// like in `web3d`) but none worked really well so we just /// hotlink the js and basically copy/paste the @types/quill /// declaration here. /// Update: we now use rollup (for html2canvas), but quill is /// still a pain so it's still not in the same bundle. const DEBUG = false; /// ^^ when debugging the quill integration, add the quill.snow.css to layout.hbs /// /// /// We tried doing it programmatically here but it's a bit slow. if (DEBUG) { document.head.insertAdjacentHTML( 'beforeend', `` ); /// ^^ add css to debug. Do it as early as possible. } enum Page { app, landing, model } const App = { page: (document.body.classList.contains('app')) ? Page.app : (document.body.classList.contains('landing')) ? Page.landing : Page.model , editable: document.body.dataset.editable === 'true', header: { shuffleBtn: document.querySelector('header .js-shuffle') as HTMLAnchorElement, triggerBtn: document.querySelector('header .js-trigger') as HTMLAnchorElement, mainInfoBtn: document.querySelector('header .title .info') as HTMLImageElement, shareBtn: document.querySelector('header .js-share'), saveBtn: document.querySelector('header .js-save'), duplicateBtn: document.querySelector('header .js-duplicate'), }, shareScreenBtn: document.querySelector('.page-container .js-share') as HTMLAnchorElement, loaderEditor: document.querySelector('.page-container .js-loader') as HTMLImageElement, sliders: Array.from( document.querySelectorAll('.decoder-settings input.slider') ) as HTMLInputElement[], INITIAL_CONTENT: {} as Delta, /** * Helper function to more cleanly route different page types. */ onLoad: (p: Page, callback: () => void) => { if (p === App.page) { document.addEventListener('DOMContentLoaded', () => { callback(); }); } }, }; const PROMPTS = [ `Before boarding your rocket to Mars, remember to pack these items`, `In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.`, `Legolas and Gimli advanced on the orcs, raising their weapons with a harrowing war cry.`, `Today, scientists confirmed the worst possible outcome: the massive asteroid will collide with Earth`, ` Thor: The Tesseract belongs on Asgard, no human is a match for it. Tony turns to leave, but Steve stops him. Steve: You're not going alone! Tony: You gonna stop me? `.replace(/\t/g, "").trim().concat("\n"), ]; App.onLoad(Page.app, () => { const modalScreenshot = new ShareScreenshotModal; const opts: QuillOptionsStatic = DEBUG ? { theme: 'snow', modules: { mention: {}, }, } : { theme: undefined, // formats: [], modules: { toolbar: [], mention: {}, }, } ; if (! App.editable) { opts.readOnly = true; } const quill = new Quill('div.editor', opts); const mention = quill.getModule('mention') as Mention; (window).quill = quill; const QUILL_C = (window).QUILL_C; if (QUILL_C) { quill.setContents(QUILL_C); } quill.container.appendChild(App.loaderEditor); quill.container.appendChild(App.shareScreenBtn); // // div.editor .ql-container <-- quill.container // +--------------------------------+ // | div.ql-editor contenteditable | <-- quill.root // | +----------------------------+ | // | | | | // | | | | // | +----------------------------+ | // +--------------------------------+ // quill.keyboard.addBinding({ key: Mention.Keys.TAB }, () => { triggerAutocomplete(); }); quill.keyboard.bindings[Mention.Keys.TAB].unshift( quill.keyboard.bindings[Mention.Keys.TAB].pop() ); /// ^^ important. /// ^^ place it at beginning of bindings. const triggerAutocomplete = async () => { /// vv position loader mention.setCursorPos(); const cursorBbox = quill.getBounds(mention.getCursorPos()); App.loaderEditor.style.top = `${cursorBbox.top - 4}px`; App.loaderEditor.style.left = `${cursorBbox.left + 4}px`; App.loaderEditor.classList.remove('hide'); /// vv Launch api request. const text = quill.getText(0, mention.getCursorPos()); // ^^ That is so much simpler that what we used to do // when we were embbedding objects like in `quill-mention`. c.debug( `%c[About to launch autocomplete for]`, `color: green;`, text, ); const o = await Api.shared.postWithSettings({ context: text }); App.loaderEditor.classList.add('hide'); /// vv Trigger mention module. for (const x of o.sentences) { c.log(x.value); } mention.trigger( o.sentences.map(x => x.value) ); }; App.header.duplicateBtn?.addEventListener('click', async (e) => { e.preventDefault(); const url = await Api.shared.postDuplicate(); window.location.href = url; }); if (! App.editable) { return ; } /** * vv Below is only in editable mode. */ const modalSave = new SavePublishModal(quill); App.header.shuffleBtn.addEventListener('click', (e) => { e.preventDefault(); quill.setText( Utils.randomItem(PROMPTS) ); quill.setSelection(quill.getLength(), 0); /// ^^ github.com/quilljs/quill/issues/2635 triggerAutocomplete(); }); App.header.triggerBtn.addEventListener('click', (e) => { e.preventDefault(); triggerAutocomplete(); }); App.header.shareBtn?.addEventListener('click', async (e) => { e.preventDefault(); const text = `Write With Transformer via @huggingface`; window.open(`https://twitter.com/share?url=${ encodeURIComponent(window.location.href) }&text=${ encodeURIComponent(text) }`); }); App.header.saveBtn?.addEventListener('click', (e) => { e.preventDefault(); mention.hideMentionList(); modalSave.show(); }); App.shareScreenBtn.addEventListener('click', async (e) => { e.preventDefault(); mention.hideMentionList(); modalScreenshot.show(); }); quill.on('text-change', () => { App.shareScreenBtn.classList.remove('hide'); /// <- we use a fadeout effect. const hasTextFromAI = quill.getContents() .ops .some(op => op.attributes && op.attributes.bold === true) ; App.shareScreenBtn.classList.toggle('fadeout', ! hasTextFromAI); }); document.addEventListener('click', (e) => { /// Handle clicks on links inside the editor. if (! ( e.target instanceof HTMLAnchorElement && e.target.closest('div.ql-editor') !== null )) { return ; } /// Ok, let's do this. e.preventDefault(); e.stopPropagation(); const href = e.target.getAttribute('href'); /// <- caution, get the original string. c.debug(`[click]`, href); if (href === '#js-shuffle') { App.header.shuffleBtn.click(); } else { window.open(e.target.href); } }); document.addEventListener("scroll", e => { const trigger = document.getElementsByClassName("js-trigger")[0] as HTMLAnchorElement; if (scrollY > 100) { trigger.style.position = "fixed"; trigger.style.top = "10px"; trigger.style.border = "1px solid blue"; trigger.style.backgroundColor = "white"; trigger.style.borderRadius = "100px"; trigger.style.padding = "5px"; trigger.style.zIndex = "1"; trigger.style.left = "50%"; trigger.style.transform = "translateX(-50%)"; } else { trigger.style.position = "relative"; trigger.style.top = "auto"; trigger.style.border = "none"; trigger.style.backgroundColor = "white"; trigger.style.borderRadius = "0"; trigger.style.padding = "0"; trigger.style.zIndex = "1"; trigger.style.left = "auto" } }); /** * Settings */ const handleSliderChange = (slider: HTMLInputElement) => { const div = slider.parentNode as HTMLDivElement; const spanVal = div.querySelector('.js-val') as HTMLSpanElement; const value = Number.isInteger(slider.valueAsNumber) ? slider.valueAsNumber : Number(slider.valueAsNumber.toFixed(2)) ; const valueKey = `value-${value}`; if (slider.dataset[valueKey]) { spanVal.innerText = slider.dataset[valueKey]!; } else { spanVal.innerText = value.toString(); } const min = Number(slider.getAttribute('min')); const max = Number(slider.getAttribute('max')); if (value < min + (max - min) / 3) { spanVal.className = "js-val green"; } else if (value < min + 2 * (max - min) / 3) { spanVal.className = "js-val orange"; } else { spanVal.className = "js-val red"; } const isInverted = slider.classList.contains('js-inverted'); if (isInverted) { if (spanVal.classList.contains('green')) { spanVal.classList.remove('green'); spanVal.classList.add('red'); } else if (spanVal.classList.contains('red')) { spanVal.classList.remove('red'); spanVal.classList.add('green'); } } }; for (const slider of App.sliders) { handleSliderChange(slider); slider.addEventListener('input', () => { handleSliderChange(slider); }); } }); App.onLoad(Page.landing, () => { /** * VanillaTilt */ VanillaTilt.init(document.querySelectorAll("[data-tilt]"), { glare: true, scale: 1.06, 'max-glare': 0.3, speed: 400, }); });