File size: 8,410 Bytes
989a61b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d881ad5
989a61b
 
 
 
 
 
 
 
d881ad5
 
 
 
 
989a61b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8ab9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
989a61b
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
(function () {

    // Target confirmed via DevTools: div.bubble-wrap[role="log"] inside #chatbot
    // scrollHeight > clientHeight = real scroll container.
    // autoscroll=False must be set in app.py β€” Gradio's own autoscroll
    // resets scrollTop after every update and overwrites our JS.

    function getContainer() {
        return document.querySelector('#chatbot .bubble-wrap');
    }

    function scrollToBottom() {
        const el = getContainer();
        if (el) el.scrollTop = el.scrollHeight;
    }

    // Keep scrolling every 100ms while content is still growing (streaming).
    // Stops 1.5s after scrollHeight stabilises.
    function pinUntilStable() {
        const el = getContainer();
        if (!el) return;

        scrollToBottom();

        let last = el.scrollHeight;
        let stable = 0;

        const id = setInterval(() => {
            const el = getContainer();
            if (!el) { clearInterval(id); return; }

            scrollToBottom();

            if (el.scrollHeight === last) {
                if (++stable >= 15) clearInterval(id);  // 15 Γ— 100ms = 1.5s stable
            } else {
                last   = el.scrollHeight;
                stable = 0;
            }
        }, 100);
    }

    function hookTriggers() {
        // Enter key in textbox
        const textarea = document.querySelector('#question-box textarea');
        if (textarea && !textarea._sh) {
            textarea._sh = true;
            textarea.addEventListener('keydown', e => {
                if (e.key === 'Enter' && !e.shiftKey) pinUntilStable();
            });
        }

        // Followup buttons β€” Gradio replaces these after each response
        document.querySelectorAll('#followups button').forEach(btn => {
            if (!btn._sh) {
                btn._sh = true;
                btn.addEventListener('click', pinUntilStable);
            }
        });
    }

    function attach() {
        const chatbot = document.querySelector('#chatbot');
        if (!chatbot) { setTimeout(attach, 300); return; }

        // Watch for new message rows β€” fires when user sends or answer arrives
        let prevRows = 0;
        new MutationObserver(() => {
            const rows = chatbot.querySelectorAll('.message-row').length;
            if (rows !== prevRows) {
                prevRows = rows;
                pinUntilStable();
            }
            hookTriggers();   // re-hook after Gradio replaces buttons
        }).observe(chatbot, { childList: true, subtree: true });

        hookTriggers();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', attach);
    } else {
        attach();
    }
})();

// ── Header carousel ───────────────────────────────────────────────────────────
// Script lives here (not in header.html) because gr.HTML sanitises <script> tags.
// Waits for the carousel DOM to be ready before initialising.

(function () {
    function initCarousel() {
        var slides = document.getElementById('arc-slides');
        var dotsEl = document.getElementById('arc-dots');
        var prev   = document.getElementById('arc-prev');
        var next   = document.getElementById('arc-next');
        if (!slides || !dotsEl || !prev || !next) {
            setTimeout(initCarousel, 200);
            return;
        }

        var total = slides.children.length;
        var cur   = 0;
        var timer;

        // Build dots
        for (var i = 0; i < total; i++) {
            var d = document.createElement('button');
            d.className = i === 0 ? 'active' : '';
            d.onclick = (function (n) { return function () { go(n); }; })(i);
            dotsEl.appendChild(d);
        }

        function go(n) {
            cur = (n + total) % total;
            slides.style.transform = 'translateX(-' + cur * 100 + '%)';
            dotsEl.querySelectorAll('button').forEach(function (d, i) {
                if (i === cur) {
                    d.classList.add('active');
                } else {
                    d.classList.remove('active');
                }
            });
            resetTimer();
        }

        function resetTimer() {
            clearInterval(timer);
            timer = setInterval(function () { go(cur + 1); }, 4500);
        }

        prev.onclick = function () { go(cur - 1); };
        next.onclick = function () { go(cur + 1); };
        resetTimer();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initCarousel);
    } else {
        initCarousel();
    }
})();

// ── Character counter ─────────────────────────────────────────────────────────
// Display remaining characters next to the "Question" label.

(function () {
    const MAX_LENGTH = 150; // Must match max_length in app.py
    
    function initCharCounter() {
        const container = document.querySelector('#question-box');
        if (!container) return;
        
        const textarea = container.querySelector('textarea');
        if (!textarea) return;
        
        // Find existing label or look for the element that contains "Question"
        let labelContainer = container.querySelector('[data-testid="block-info"]');
        
        // If not found, try to find the label by looking for text nodes
        if (!labelContainer) {
            // Try finding the label in the container's structure
            const labels = container.querySelectorAll('label');
            if (labels.length > 0) {
                labelContainer = labels[0].parentElement;
            }
        }
        
        // If still not found, just create a container above the textarea
        if (!labelContainer) {
            labelContainer = document.createElement('div');
            labelContainer.className = 'label-container';
            
            const label = document.createElement('span');
            label.textContent = 'Question';
            labelContainer.appendChild(label);
            
            textarea.parentElement.insertBefore(labelContainer, textarea);
        }
        
        // Create or get counter element
        let counter = labelContainer.querySelector('.char-counter');
        if (!counter) {
            counter = document.createElement('div');
            counter.className = 'char-counter';
            labelContainer.appendChild(counter);
        }
        
        // Enforce maxlength at browser level too
        textarea.maxLength = MAX_LENGTH;

        function updateCount() {
            if (textarea.value.length > MAX_LENGTH) {
                textarea.value = textarea.value.slice(0, MAX_LENGTH);
            }
            const len = textarea.value.length;
            const remaining = MAX_LENGTH - len;
            counter.textContent = `${len} / ${MAX_LENGTH}`;
            
            // Update classes for styling
            counter.classList.remove('warning', 'critical');
            if (remaining < 50) {
                counter.classList.add('critical');
            } else if (remaining < 100) {
                counter.classList.add('warning');
            }
        }
        
        if (!textarea._counterSetup) {
            textarea._counterSetup = true;
            textarea.addEventListener('input', updateCount);
            textarea.addEventListener('change', updateCount);
            textarea.addEventListener('keyup', updateCount);

            // Keep in sync when Gradio or app clears content programmatically
            textarea._counterInterval = setInterval(updateCount, 200);
            updateCount();  // initial
        }
    }
    
    // Wait for DOM to be ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initCharCounter);
    } else {
        initCharCounter();
    }
    
    // Retry periodically in case Gradio recreates the element
    setInterval(initCharCounter, 500);
})();