shivam11 commited on
Commit
d8bde56
·
verified ·
1 Parent(s): cbfb287

Deploy from mauritius-budget-pensions/app

Browse files
Files changed (2) hide show
  1. app.py +7 -43
  2. ask.js +113 -0
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
- ASK_JS = """
575
- () => {
576
- const fire = (el, type, init) => el.dispatchEvent(
577
- new (type === 'keydown' ? KeyboardEvent : Event)(type,
578
- Object.assign({bubbles: true}, init || {})));
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
- def _prefill(request: gr.Request):
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
+ }