Deploy from mauritius-budget-pensions/app
Browse files
app.py
CHANGED
|
@@ -571,43 +571,12 @@ def respond(message, history):
|
|
| 571 |
# Submitting is done two ways on purpose. Gradio renders its send control
|
| 572 |
# differently across versions, so try the button, and fall back to an Enter
|
| 573 |
# keypress on the textarea, which is the path the reader's own keyboard takes.
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
document.addEventListener('click', (ev) => {
|
| 581 |
-
const a = ev.target.closest && ev.target.closest('a.askref');
|
| 582 |
-
if (!a) return;
|
| 583 |
-
ev.preventDefault();
|
| 584 |
-
ev.stopPropagation();
|
| 585 |
-
let q = '';
|
| 586 |
-
try { q = decodeURIComponent(a.dataset.q || ''); } catch (e) { q = a.dataset.q || ''; }
|
| 587 |
-
if (!q) return;
|
| 588 |
-
|
| 589 |
-
const box = document.querySelector('#askbox textarea, #askbox input[type=text]');
|
| 590 |
-
if (!box) return;
|
| 591 |
-
// React/Svelte track the value on the node, so write through the native
|
| 592 |
-
// setter -- assigning .value directly leaves the framework's copy stale and
|
| 593 |
-
// the submitted message comes out empty.
|
| 594 |
-
const proto = box.tagName === 'TEXTAREA'
|
| 595 |
-
? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
|
| 596 |
-
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
| 597 |
-
setter.call(box, q);
|
| 598 |
-
fire(box, 'input');
|
| 599 |
-
box.focus();
|
| 600 |
-
|
| 601 |
-
const btn = document.querySelector(
|
| 602 |
-
'#askbox button[title="Submit"], #askbox button[aria-label="Submit"], ' +
|
| 603 |
-
'#askbox .submit-button, .chat-input-container button[title="Submit"]');
|
| 604 |
-
setTimeout(() => {
|
| 605 |
-
if (btn) { btn.click(); }
|
| 606 |
-
else { fire(box, 'keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13}); }
|
| 607 |
-
}, 30);
|
| 608 |
-
}, true);
|
| 609 |
-
}
|
| 610 |
-
"""
|
| 611 |
|
| 612 |
def build(cls, **kw):
|
| 613 |
"""Construct a Gradio component, dropping kwargs this version rejects.
|
|
@@ -678,12 +647,7 @@ demo = build(
|
|
| 678 |
# box is pre-filled rather than auto-sent: the reader chooses whether to spend
|
| 679 |
# the call, and can edit the question first.
|
| 680 |
with demo:
|
| 681 |
-
|
| 682 |
-
return (request.query_params.get("q") or "")[:700]
|
| 683 |
-
|
| 684 |
-
demo.load(_prefill, None, demo.textbox)
|
| 685 |
-
|
| 686 |
-
# Install the cross-reference click handler. ChatInterface itself does not
|
| 687 |
# take a js= argument in Gradio 6 -- it is silently not a constructor
|
| 688 |
# parameter -- but the load event does, so the script goes on here. If even
|
| 689 |
# that changes, say so loudly: the links would still render and simply stop
|
|
|
|
| 571 |
# Submitting is done two ways on purpose. Gradio renders its send control
|
| 572 |
# differently across versions, so try the button, and fall back to an Enter
|
| 573 |
# keypress on the textarea, which is the path the reader's own keyboard takes.
|
| 574 |
+
# The page's own behaviour: cross-reference links that ask in this session,
|
| 575 |
+
# ?q= asked automatically on arrival, and landing on the top of each new answer.
|
| 576 |
+
# Kept in app/ask.js so it can be exercised against a stub DOM by
|
| 577 |
+
# scripts/test_ask_js.mjs -- it is JavaScript, and testing it as a Python string
|
| 578 |
+
# would mean testing nothing.
|
| 579 |
+
ASK_JS = (Path(__file__).resolve().parent / "ask.js").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
|
| 581 |
def build(cls, **kw):
|
| 582 |
"""Construct a Gradio component, dropping kwargs this version rejects.
|
|
|
|
| 647 |
# box is pre-filled rather than auto-sent: the reader chooses whether to spend
|
| 648 |
# the call, and can edit the question first.
|
| 649 |
with demo:
|
| 650 |
+
# Install the page's behaviour (links, auto-ask, scroll). ChatInterface itself does not
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
# take a js= argument in Gradio 6 -- it is silently not a constructor
|
| 652 |
# parameter -- but the load event does, so the script goes on here. If even
|
| 653 |
# that changes, say so loudly: the links would still render and simply stop
|
ask.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Client-side behaviour for the archive chat, loaded by app.py via demo.load(js=).
|
| 2 |
+
//
|
| 3 |
+
// Three jobs, all of them about a reader following a reference rather than
|
| 4 |
+
// typing a question:
|
| 5 |
+
//
|
| 6 |
+
// 1. A cross-reference in an answer is a link that asks the question HERE.
|
| 7 |
+
// An ordinary ?q= href would open a new tab and start a fresh session,
|
| 8 |
+
// discarding the conversation the reader is in the middle of.
|
| 9 |
+
// 2. A ?q= on the URL -- how the dashboard links every question -- is asked
|
| 10 |
+
// automatically on arrival. The reader followed a link to get an answer;
|
| 11 |
+
// making them press send again is a step with no decision in it.
|
| 12 |
+
// 3. After each answer, the view goes to the TOP of that answer. Gradio pins
|
| 13 |
+
// a chat to the bottom, which for a long reply means landing on its last
|
| 14 |
+
// line and having to scroll back up to read it from the start.
|
| 15 |
+
//
|
| 16 |
+
// Kept in its own file so it can be exercised by scripts/test_ask_js.mjs
|
| 17 |
+
// against a stub DOM; app.py inlines it at import.
|
| 18 |
+
|
| 19 |
+
() => {
|
| 20 |
+
const SUBMIT_SELECTOR = [
|
| 21 |
+
'#askbox button[title="Submit"]',
|
| 22 |
+
'#askbox button[aria-label="Submit"]',
|
| 23 |
+
'#askbox .submit-button',
|
| 24 |
+
'.chat-input-container button[title="Submit"]',
|
| 25 |
+
].join(', ');
|
| 26 |
+
|
| 27 |
+
// Gradio's markup for a bot turn has changed shape across versions, so try
|
| 28 |
+
// the stable hooks first and fall back to anything labelled as a bot row.
|
| 29 |
+
const BOT_SELECTOR = [
|
| 30 |
+
'#chatbox [data-testid="bot"]',
|
| 31 |
+
'#chatbox .bot',
|
| 32 |
+
'#chatbox .message.bot',
|
| 33 |
+
'#chatbox .message-row.bot-row',
|
| 34 |
+
].join(', ');
|
| 35 |
+
|
| 36 |
+
const fire = (el, type, init) => el.dispatchEvent(
|
| 37 |
+
new (type === 'keydown' ? KeyboardEvent : Event)(type,
|
| 38 |
+
Object.assign({ bubbles: true }, init || {})));
|
| 39 |
+
|
| 40 |
+
const box = () => document.querySelector('#askbox textarea, #askbox input[type=text]');
|
| 41 |
+
|
| 42 |
+
// Poll rather than assume: this runs when the app mounts, and the textbox is
|
| 43 |
+
// not necessarily in the DOM yet on a cold Space.
|
| 44 |
+
function waitFor(get, ms, done) {
|
| 45 |
+
const started = Date.now();
|
| 46 |
+
(function tick() {
|
| 47 |
+
const el = get();
|
| 48 |
+
if (el) { done(el); return; }
|
| 49 |
+
if (Date.now() - started > ms) return;
|
| 50 |
+
setTimeout(tick, 60);
|
| 51 |
+
})();
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function ask(text) {
|
| 55 |
+
if (!text) return;
|
| 56 |
+
waitFor(box, 8000, (el) => {
|
| 57 |
+
// React/Svelte track the value on the node, so write through the native
|
| 58 |
+
// setter -- assigning .value directly leaves the framework's copy stale
|
| 59 |
+
// and the submitted message comes out empty.
|
| 60 |
+
const proto = el.tagName === 'TEXTAREA'
|
| 61 |
+
? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
|
| 62 |
+
Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, text);
|
| 63 |
+
fire(el, 'input');
|
| 64 |
+
el.focus();
|
| 65 |
+
setTimeout(() => {
|
| 66 |
+
const btn = document.querySelector(SUBMIT_SELECTOR);
|
| 67 |
+
if (btn) btn.click();
|
| 68 |
+
else fire(el, 'keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13 });
|
| 69 |
+
}, 30);
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// --- 1. cross-reference links -------------------------------------------
|
| 74 |
+
document.addEventListener('click', (ev) => {
|
| 75 |
+
const a = ev.target.closest && ev.target.closest('a.askref');
|
| 76 |
+
if (!a) return;
|
| 77 |
+
ev.preventDefault();
|
| 78 |
+
ev.stopPropagation();
|
| 79 |
+
let q = a.dataset.q || '';
|
| 80 |
+
try { q = decodeURIComponent(q); } catch (e) { /* use it raw */ }
|
| 81 |
+
ask(q);
|
| 82 |
+
}, true);
|
| 83 |
+
|
| 84 |
+
// --- 2. ?q= is asked on arrival -----------------------------------------
|
| 85 |
+
let incoming = '';
|
| 86 |
+
try {
|
| 87 |
+
incoming = new URLSearchParams(window.location.search).get('q') || '';
|
| 88 |
+
} catch (e) { incoming = ''; }
|
| 89 |
+
if (incoming) ask(incoming.slice(0, 700));
|
| 90 |
+
|
| 91 |
+
// --- 3. land on the top of the newest answer ----------------------------
|
| 92 |
+
let lastSeen = null;
|
| 93 |
+
let timer = null;
|
| 94 |
+
|
| 95 |
+
function toLatestAnswer() {
|
| 96 |
+
const answers = document.querySelectorAll(BOT_SELECTOR);
|
| 97 |
+
const latest = answers[answers.length - 1];
|
| 98 |
+
// Only when the newest answer is one we have not already moved to, or the
|
| 99 |
+
// page would fight the reader every time they scrolled away from it.
|
| 100 |
+
if (!latest || latest === lastSeen) return;
|
| 101 |
+
lastSeen = latest;
|
| 102 |
+
latest.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
waitFor(() => document.querySelector('#chatbox'), 8000, (chat) => {
|
| 106 |
+
// Debounced: an answer arrives as several mutations, and scrolling on each
|
| 107 |
+
// one lands somewhere arbitrary in the middle of it.
|
| 108 |
+
new MutationObserver(() => {
|
| 109 |
+
clearTimeout(timer);
|
| 110 |
+
timer = setTimeout(toLatestAnswer, 250);
|
| 111 |
+
}).observe(chat, { childList: true, subtree: true });
|
| 112 |
+
});
|
| 113 |
+
}
|