infinityonline commited on
Commit
157b8da
ยท
verified ยท
1 Parent(s): b06958b

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +199 -143
main.py CHANGED
@@ -64,7 +64,6 @@ class AsyncBrowserThread(threading.Thread):
64
  "--no-zygote",
65
  ],
66
  )
67
-
68
  self.persistent_context = await self.browser.new_context(
69
  user_agent=(
70
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
@@ -76,7 +75,7 @@ class AsyncBrowserThread(threading.Thread):
76
  await self.persistent_context.add_init_script(
77
  "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
78
  )
79
-
80
  setup_page = await self.persistent_context.new_page()
81
  try:
82
  print("[SERVER] Opening duck.ai for setup...")
@@ -98,10 +97,197 @@ class AsyncBrowserThread(threading.Thread):
98
  finally:
99
  await setup_page.close()
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  async def _chat(self, model_label: str, prompt: str) -> str:
102
  page = await self.persistent_context.new_page()
103
  try:
104
- # !! timeout ูˆุงุญุฏ ูƒุจูŠุฑ ู„ูƒู„ ุงู„ุตูุญุฉ โ€” ู„ุง ูŠูุบูŠูŽู‘ุฑ ุฃุจุฏุงู‹
105
  page.set_default_timeout(180000)
106
 
107
  # โ”€โ”€ 1. ูุชุญ ุงู„ุตูุญุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -125,155 +311,25 @@ class AsyncBrowserThread(threading.Thread):
125
  print("[DUCK] Input ready โœ“")
126
 
127
  # โ”€โ”€ 4. ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
128
- try:
129
- model_btn = page.locator('button[data-testid="model-select-button"]')
130
- current_text = await model_btn.inner_text()
131
- print(f"[DUCK] Current model: {current_text.strip()}")
132
 
133
- if model_label.lower() not in current_text.lower():
134
- await model_btn.click()
135
- await asyncio.sleep(2)
136
-
137
- option = page.locator(
138
- f"li:has-text('{model_label}'), "
139
- f"[role='option']:has-text('{model_label}')"
140
- )
141
- if await option.count() > 0:
142
- await option.first.click()
143
- print(f"[DUCK] Model โ†’ {model_label} โœ“")
144
- else:
145
- await page.keyboard.press("Escape")
146
- print("[DUCK] Model not found, using default")
147
-
148
- await asyncio.sleep(3)
149
- await page.wait_for_selector(
150
- 'textarea[name="user-prompt"]', state="visible"
151
- )
152
- print("[DUCK] Textarea re-confirmed โœ“")
153
-
154
- except Exception as e:
155
- print(f"[DUCK] Model select (non-fatal): {e}")
156
-
157
- # โ”€โ”€ 5. ุญู‚ู† ุงู„ู†ุต ุนุจุฑ JavaScript โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
158
- await page.evaluate(
159
- """
160
- (text) => {
161
- const ta = document.querySelector('textarea[name="user-prompt"]');
162
- if (!ta) return;
163
- const nativeSetter = Object.getOwnPropertyDescriptor(
164
- window.HTMLTextAreaElement.prototype, 'value'
165
- ).set;
166
- nativeSetter.call(ta, text);
167
- ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
168
- ta.dispatchEvent(new Event('change', { bubbles: true }));
169
- ta.focus();
170
- }
171
- """,
172
- prompt
173
- )
174
- await asyncio.sleep(2)
175
-
176
- # โ”€โ”€ 6. ุฅุฑุณุงู„ ุงู„ุฑุณุงู„ุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
177
- # ู…ุญุงูˆู„ุฉ 1: ุฒุฑ Submit
178
- sent = False
179
- try:
180
- send_btn = page.locator('button[type="submit"][aria-label="Send"]')
181
- is_disabled = await send_btn.get_attribute("disabled")
182
- if is_disabled is None:
183
- await send_btn.click()
184
- print(f"[DUCK] Sent via button โœ“")
185
- sent = True
186
- except Exception:
187
- pass
188
-
189
- # ู…ุญุงูˆู„ุฉ 2: Enter ู…ุจุงุดุฑุฉ
190
  if not sent:
191
- ta = page.locator('textarea[name="user-prompt"]')
192
- await ta.click()
193
- await asyncio.sleep(0.5)
194
- await page.keyboard.press("Enter")
195
- print(f"[DUCK] Sent via Enter โœ“")
196
-
197
- print(f"[DUCK] Prompt length: {len(prompt)} chars")
198
 
199
- # โ”€โ”€ 7. ุงู†ุชุธุงุฑ ุจุฏุก ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
200
- await asyncio.sleep(3)
201
- response_started = False
202
- try:
203
- stop_btn = page.locator('button[aria-label="Stop generating"]')
204
- await stop_btn.wait_for(state="visible", timeout=30000)
205
- print("[DUCK] Response started โœ“")
206
- response_started = True
207
- except Exception:
208
- print("[DUCK] Stop btn not detected, watching for content...")
209
-
210
- # โ”€โ”€ 8. ุงู†ุชุธุงุฑ ุงูƒุชู…ุงู„ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
211
- max_wait = 150
212
- elapsed = 0
213
-
214
- if response_started:
215
- while elapsed < max_wait:
216
- await asyncio.sleep(2)
217
- elapsed += 2
218
- still_running = await page.locator(
219
- 'button[aria-label="Stop generating"]:not([disabled])'
220
- ).count()
221
- if still_running == 0:
222
- print(f"[DUCK] Complete (~{elapsed}s) โœ“")
223
- break
224
- else:
225
- # ุงู†ุชุธุฑ ุธู‡ูˆุฑ article
226
- while elapsed < max_wait:
227
- await asyncio.sleep(2)
228
- elapsed += 2
229
- if await page.locator('article').count() > 0:
230
- await asyncio.sleep(6)
231
- print(f"[DUCK] Article appeared (~{elapsed}s) โœ“")
232
- break
233
-
234
- await asyncio.sleep(2)
235
-
236
- # โ”€โ”€ 9. ุงุณุชุฎุฑุงุฌ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
237
- response_text = await page.evaluate("""
238
- () => {
239
- const articles = document.querySelectorAll('article');
240
- if (articles.length > 0) {
241
- return articles[articles.length - 1].innerText.trim();
242
- }
243
- const msgDivs = document.querySelectorAll(
244
- '[class*="message"]:not([class*="user"])' +
245
- ':not([class*="User"]):not([class*="input"])'
246
- );
247
- if (msgDivs.length > 0) {
248
- const valid = [...msgDivs].filter(el =>
249
- el.innerText && el.innerText.trim().length > 20 &&
250
- !el.querySelector('textarea')
251
- );
252
- if (valid.length > 0) {
253
- return valid[valid.length - 1].innerText.trim();
254
- }
255
- }
256
- const divs = [...document.querySelectorAll('div')].filter(el =>
257
- el.children.length < 10 &&
258
- el.innerText && el.innerText.trim().length > 50 &&
259
- !el.querySelector('textarea') &&
260
- !el.querySelector('button[type="submit"]')
261
- );
262
- if (divs.length > 0) {
263
- return divs.sort(
264
- (a, b) => b.innerText.length - a.innerText.length
265
- )[0].innerText.trim();
266
- }
267
- return '';
268
- }
269
- """)
270
 
271
- if not response_text or len(response_text.strip()) < 10:
 
272
  await asyncio.sleep(5)
273
  response_text = await page.evaluate("""
274
  () => {
275
  const arts = document.querySelectorAll('article');
276
  if (arts.length > 0) return arts[arts.length-1].innerText.trim();
 
 
277
  return document.body.innerText.slice(0, 5000);
278
  }
279
  """)
 
64
  "--no-zygote",
65
  ],
66
  )
 
67
  self.persistent_context = await self.browser.new_context(
68
  user_agent=(
69
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
 
75
  await self.persistent_context.add_init_script(
76
  "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
77
  )
78
+ # ู‚ุจูˆู„ ุงู„ุดุฑูˆุท ู…ุฑุฉ ูˆุงุญุฏุฉ
79
  setup_page = await self.persistent_context.new_page()
80
  try:
81
  print("[SERVER] Opening duck.ai for setup...")
 
97
  finally:
98
  await setup_page.close()
99
 
100
+ async def _select_model(self, page, model_label: str):
101
+ """
102
+ ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ ุนุจุฑ:
103
+ 1. Ctrl+Alt+M ู„ูุชุญ ู‚ุงุฆู…ุฉ ุงู„ู†ู…ุงุฐุฌ
104
+ 2. ุงุฎุชูŠุงุฑ ุงู„ู†ู…ูˆุฐุฌ
105
+ 3. ุงู„ุถุบุท ุนู„ู‰ "Start chat"
106
+ """
107
+ try:
108
+ model_btn = page.locator('button[data-testid="model-select-button"]')
109
+ current_text = (await model_btn.inner_text()).strip()
110
+ print(f"[DUCK] Current model: {current_text}")
111
+
112
+ if model_label.lower() in current_text.lower():
113
+ print(f"[DUCK] Model already correct โœ“")
114
+ return True
115
+
116
+ # ูุชุญ ู‚ุงุฆู…ุฉ ุงู„ู†ู…ุงุฐุฌ
117
+ await model_btn.click()
118
+ await asyncio.sleep(2)
119
+
120
+ # ุงู„ุจุญุซ ุนู† ุงู„ู†ู…ูˆุฐุฌ ููŠ ุงู„ู‚ุงุฆู…ุฉ
121
+ # ู…ู† HTML: ู‚ุงุฆู…ุฉ ููŠู‡ุง ุงุณู… ุงู„ู†ู…ูˆุฐุฌ ูƒู†ุต
122
+ option = page.locator(
123
+ f'li:has-text("{model_label}"), '
124
+ f'[role="option"]:has-text("{model_label}"), '
125
+ f'button:has-text("{model_label}")'
126
+ )
127
+
128
+ if await option.count() > 0:
129
+ await option.first.click()
130
+ print(f"[DUCK] Model selected: {model_label} โœ“")
131
+ await asyncio.sleep(1)
132
+
133
+ # ุงู„ุจุญุซ ุนู† ุฒุฑ "Start chat" ุจุนุฏ ุงุฎุชูŠุงุฑ ุงู„ู†ู…ูˆุฐุฌ
134
+ start_btn = page.locator(
135
+ 'button:has-text("Start chat"), '
136
+ 'button:has-text("Start new chat")'
137
+ )
138
+ if await start_btn.count() > 0:
139
+ await start_btn.first.click()
140
+ print("[DUCK] Start chat clicked โœ“")
141
+ await asyncio.sleep(2)
142
+
143
+ # ุงู†ุชุธุฑ textarea ุจุนุฏ ุงู„ุชุบูŠูŠุฑ
144
+ await page.wait_for_selector(
145
+ 'textarea[name="user-prompt"]',
146
+ state="visible",
147
+ timeout=15000
148
+ )
149
+ print("[DUCK] Textarea ready after model change โœ“")
150
+ return True
151
+ else:
152
+ await page.keyboard.press("Escape")
153
+ print(f"[DUCK] Model '{model_label}' not in list, using default")
154
+ return False
155
+
156
+ except Exception as e:
157
+ print(f"[DUCK] Model select error (non-fatal): {e}")
158
+ return False
159
+
160
+ async def _inject_and_send(self, page, prompt: str) -> bool:
161
+ """
162
+ ุญู‚ู† ุงู„ู†ุต ููŠ textarea ูˆุฅุฑุณุงู„ู‡
163
+ """
164
+ # ุญู‚ู† ุงู„ู†ุต ุนุจุฑ JavaScript ู…ุน React events
165
+ await page.evaluate(
166
+ """
167
+ (text) => {
168
+ const ta = document.querySelector('textarea[name="user-prompt"]');
169
+ if (!ta) return;
170
+ const nativeSetter = Object.getOwnPropertyDescriptor(
171
+ window.HTMLTextAreaElement.prototype, 'value'
172
+ ).set;
173
+ nativeSetter.call(ta, text);
174
+ ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
175
+ ta.dispatchEvent(new Event('change', { bubbles: true }));
176
+ ta.focus();
177
+ }
178
+ """,
179
+ prompt
180
+ )
181
+ await asyncio.sleep(2)
182
+
183
+ # ู…ุญุงูˆู„ุฉ 1: ุฒุฑ Submit
184
+ try:
185
+ send_btn = page.locator('button[type="submit"][aria-label="Send"]')
186
+ is_disabled = await send_btn.get_attribute("disabled")
187
+ if is_disabled is None:
188
+ await send_btn.click()
189
+ print(f"[DUCK] Sent via button โœ“ ({len(prompt)} chars)")
190
+ return True
191
+ except Exception:
192
+ pass
193
+
194
+ # ู…ุญุงูˆู„ุฉ 2: Enter ุนู„ู‰ ุงู„ู€ textarea
195
+ try:
196
+ ta = page.locator('textarea[name="user-prompt"]')
197
+ await ta.click()
198
+ await asyncio.sleep(0.3)
199
+ await page.keyboard.press("Enter")
200
+ print(f"[DUCK] Sent via Enter โœ“ ({len(prompt)} chars)")
201
+ return True
202
+ except Exception as e:
203
+ print(f"[DUCK] Send failed: {e}")
204
+ return False
205
+
206
+ async def _wait_for_response(self, page) -> str:
207
+ """
208
+ ุงู†ุชุธุงุฑ ุงูƒุชู…ุงู„ ุงู„ุฑุฏ ูˆุงุณุชุฎุฑุงุฌู‡
209
+ """
210
+ # ุงู†ุชุธุฑ ุธู‡ูˆุฑ ุงู„ุฑุฏ
211
+ await asyncio.sleep(3)
212
+
213
+ # ุงู†ุชุธุฑ ุฒุฑ Copy ุจู€ data-copyairesponse="true"
214
+ # ู‡ุฐุง ูŠุธู‡ุฑ ูู‚ุท ุจุนุฏ ุงูƒุชู…ุงู„ ุงู„ุฑุฏ ุงู„ุฃุฎูŠุฑ
215
+ try:
216
+ copy_btn = page.locator('button[data-copyairesponse="true"]')
217
+ await copy_btn.last.wait_for(state="visible", timeout=90000)
218
+ print("[DUCK] Response complete (copy button appeared) โœ“")
219
+ await asyncio.sleep(1)
220
+ except Exception:
221
+ print("[DUCK] Copy button not detected, using fallback wait...")
222
+ # fallback: ุงู†ุชุธุฑ ุงุฎุชูุงุก Stop button
223
+ max_wait = 120
224
+ elapsed = 0
225
+ while elapsed < max_wait:
226
+ await asyncio.sleep(2)
227
+ elapsed += 2
228
+ stop_active = await page.locator(
229
+ 'button[aria-label="Stop generating"]:not([disabled])'
230
+ ).count()
231
+ if stop_active == 0:
232
+ print(f"[DUCK] Response complete (stop gone ~{elapsed}s) โœ“")
233
+ break
234
+
235
+ await asyncio.sleep(1)
236
+
237
+ # ุงุณุชุฎุฑุงุฌ ุงู„ู†ุต ู…ู† ุงู„ุจู†ูŠุฉ ุงู„ุญู‚ูŠู‚ูŠุฉ
238
+ # ู…ู† HTML: div[data-activeresponse="true"] > ... > div.space-y-4
239
+ response_text = await page.evaluate("""
240
+ () => {
241
+ // ุทุฑูŠู‚ุฉ 1: div[data-activeresponse="true"] โ€” ุงู„ุจู†ูŠุฉ ุงู„ุญู‚ูŠู‚ูŠุฉ ู„ู€ duck.ai
242
+ const activeResp = document.querySelector('[data-activeresponse="true"]');
243
+ if (activeResp) {
244
+ // ุงู„ู†ุต ููŠ div.space-y-4 ุฏุงุฎู„ ุงู„ุฑุฏ
245
+ const textDiv = activeResp.querySelector('.space-y-4');
246
+ if (textDiv && textDiv.innerText.trim().length > 0) {
247
+ return textDiv.innerText.trim();
248
+ }
249
+ // ุฃูˆ ุงู„ู†ุต ุงู„ู…ุจุงุดุฑ
250
+ const prose = activeResp.querySelector(
251
+ '[class*="whitespace-normal"], [class*="prose"]'
252
+ );
253
+ if (prose && prose.innerText.trim().length > 0) {
254
+ return prose.innerText.trim();
255
+ }
256
+ }
257
+
258
+ // ุทุฑูŠู‚ุฉ 2: ุขุฎุฑ div[data-activeresponse]
259
+ const allResps = document.querySelectorAll('[data-activeresponse]');
260
+ if (allResps.length > 0) {
261
+ const last = allResps[allResps.length - 1];
262
+ const textDiv = last.querySelector('.space-y-4');
263
+ if (textDiv) return textDiv.innerText.trim();
264
+ return last.innerText.trim();
265
+ }
266
+
267
+ // ุทุฑูŠู‚ุฉ 3: article
268
+ const articles = document.querySelectorAll('article');
269
+ if (articles.length > 0) {
270
+ return articles[articles.length - 1].innerText.trim();
271
+ }
272
+
273
+ // ุทุฑูŠู‚ุฉ 4: div.space-y-4 ุงู„ุฃุฎูŠุฑ
274
+ const spaceDivs = document.querySelectorAll('.space-y-4');
275
+ if (spaceDivs.length > 0) {
276
+ for (let i = spaceDivs.length - 1; i >= 0; i--) {
277
+ const text = spaceDivs[i].innerText.trim();
278
+ if (text.length > 10) return text;
279
+ }
280
+ }
281
+
282
+ return '';
283
+ }
284
+ """)
285
+
286
+ return response_text.strip()
287
+
288
  async def _chat(self, model_label: str, prompt: str) -> str:
289
  page = await self.persistent_context.new_page()
290
  try:
 
291
  page.set_default_timeout(180000)
292
 
293
  # โ”€โ”€ 1. ูุชุญ ุงู„ุตูุญุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
311
  print("[DUCK] Input ready โœ“")
312
 
313
  # โ”€โ”€ 4. ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
314
+ await self._select_model(page, model_label)
 
 
 
315
 
316
+ # โ”€โ”€ 5. ุฅุฑุณุงู„ ุงู„ุฑุณุงู„ุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
317
+ sent = await self._inject_and_send(page, prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  if not sent:
319
+ raise RuntimeError("Failed to send message")
 
 
 
 
 
 
320
 
321
+ # โ”€โ”€ 6. ุงู†ุชุธุงุฑ ูˆุงุณุชุฎุฑุงุฌ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
322
+ response_text = await self._wait_for_response(page)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
+ # fallback ุฅุฐุง ูƒุงู† ูุงุฑุบุงู‹
325
+ if not response_text or len(response_text.strip()) < 5:
326
  await asyncio.sleep(5)
327
  response_text = await page.evaluate("""
328
  () => {
329
  const arts = document.querySelectorAll('article');
330
  if (arts.length > 0) return arts[arts.length-1].innerText.trim();
331
+ const sp = document.querySelectorAll('.space-y-4');
332
+ if (sp.length > 0) return sp[sp.length-1].innerText.trim();
333
  return document.body.innerText.slice(0, 5000);
334
  }
335
  """)