File size: 4,805 Bytes
5f1356b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/* LilyScript editor mount + state bridge.
 *
 * Mounts our own CodeMirror 6 editor (window.LilyEditor, from lyl-editor.bundle.js)
 * into #ls-editor-mount and bridges it to a hidden Gradio Textbox (#ls-editor-state),
 * which is the canonical text Gradio reads/writes:
 *
 *   CM edit      -> write hidden <textarea>.value + dispatch native 'input'  -> Gradio
 *                   updates the Python value and fires its .change -> SVG render.
 *   Gradio write -> generation stream / file load set the <textarea>.value; we poll
 *                   for external changes and push them into CM via setValue().
 *
 * An `applyingExternal` flag prevents the echo loop (CM.setValue must not re-emit a
 * change back to the textarea), mirroring live-editor's `isEditorUpdate` guard.
 */
(function () {
	'use strict';

	var MOUNT_ID = 'ls-editor-mount';
	var STATE_ID = 'ls-editor-state';
	var DEBOUNCE_MS = 300;

	var handle = null;        // { getValue, setValue, destroy }
	var textarea = null;
	var lastTextareaValue = null;   // last value we've seen on the textarea
	var applyingExternal = false;   // true while pushing a Gradio value into CM
	var debounceTimer = null;

	function log (msg) { console.log('[lyl-editor]', msg); }

	function ready () {
		return window.LilyEditor && typeof window.LilyEditor.mount === 'function';
	}

	// Write text into the hidden textarea using the native setter (so Svelte/Gradio's
	// controlled <textarea> actually registers it) and dispatch a bubbling 'input'.
	function pushToGradio (text) {
		if (!textarea) return;
		var proto = window.HTMLTextAreaElement && window.HTMLTextAreaElement.prototype;
		var setter = proto && Object.getOwnPropertyDescriptor(proto, 'value');
		if (setter && setter.set) setter.set.call(textarea, text);
		else textarea.value = text;
		lastTextareaValue = text;
		textarea.dispatchEvent(new Event('input', { bubbles: true }));
		textarea.dispatchEvent(new Event('change', { bubbles: true }));
	}

	// CM editor -> Gradio (debounced). Skipped while we're applying an external value.
	function onEditorChange (text) {
		if (applyingExternal) return;
		if (debounceTimer) clearTimeout(debounceTimer);
		debounceTimer = setTimeout(function () { pushToGradio(text); }, DEBOUNCE_MS);
	}

	// Gradio -> CM: when the textarea value changes from OUTSIDE (generation stream,
	// file load), push it into the editor. We diff against lastTextareaValue so our
	// own pushToGradio writes don't bounce back.
	function pollExternal () {
		if (!textarea || !handle) return;
		var v = textarea.value;
		if (v === lastTextareaValue) return;   // unchanged, or our own write
		lastTextareaValue = v;
		applyingExternal = true;
		handle.setValue(v);                    // no-op guarded inside if equal
		applyingExternal = false;
	}

	function attach () {
		var mountEl = document.getElementById(MOUNT_ID);
		var stateWrap = document.getElementById(STATE_ID);
		if (!mountEl || !stateWrap || !ready()) return false;
		// gr.Textbox renders a <textarea> inside its elem_id wrapper.
		var ta = stateWrap.querySelector('textarea');
		if (!ta) return false;
		if (handle && textarea === ta && mountEl.firstChild) return true; // already mounted

		textarea = ta;
		lastTextareaValue = textarea.value;
		// Gradio wraps gr.HTML content in a `.prose` block whose global rule
		// `.gradio-container .prose * { color: var(--body-text-color) }` overrides
		// CodeMirror's per-token syntax colours (it out-specifies CM's atomic class
		// rules). Strip `prose` from the editor's wrapper so CM's oneDark highlight
		// colours win. (Scoped to the editor; the score panel keeps its own.)
		var proseWrap = mountEl.closest('.prose');
		if (proseWrap) proseWrap.classList.remove('prose');
		mountEl.innerHTML = '';
		handle = window.LilyEditor.mount(mountEl, {
			value: textarea.value || '',
			onChange: onEditorChange,
		});
		log('mounted CM editor; initial ' + (textarea.value || '').length + ' chars');
		return true;
	}

	function boot () {
		var mounted = attach();
		if (!mounted) {
			var iv = setInterval(function () { if (attach()) clearInterval(iv); }, 300);
			setTimeout(function () { clearInterval(iv); }, 30000);
		}
		// poll the hidden textarea for external (Gradio-driven) value changes, and
		// re-attach if Gradio re-renders the editor DOM (mount/textarea swapped).
		setInterval(function () {
			var mountEl = document.getElementById(MOUNT_ID);
			var stateWrap = document.getElementById(STATE_ID);
			var ta = stateWrap && stateWrap.querySelector('textarea');
			if (mountEl && ta && (ta !== textarea || !mountEl.firstChild)) { attach(); return; }
			pollExternal();
		}, 200);
	}

	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', boot);
	} else {
		boot();
	}
	log('lyl-editor-mount.js loaded');
})();