koneck 2.92.0 → 2.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/model-catalog.d.ts +5 -1
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +49 -2
- package/dist/model-catalog.js.map +1 -1
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +15 -1
- package/dist/web/api.js.map +1 -1
- package/dist/web/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +414 -87
- package/dist/web/ui-client.js.map +1 -1
- package/dist/web/ui-css.d.ts.map +1 -1
- package/dist/web/ui-css.js +42 -0
- package/dist/web/ui-css.js.map +1 -1
- package/package.json +1 -1
package/dist/web/ui-client.js
CHANGED
|
@@ -4446,7 +4446,15 @@ $('tfontdown').onclick = () => { term.fontPx = Math.max(9, term.fontPx - 1); app
|
|
|
4446
4446
|
* is dispatched as a real mouse event at the same point, so what happens is what would happen to a
|
|
4447
4447
|
* person clicking there.
|
|
4448
4448
|
*/
|
|
4449
|
-
|
|
4449
|
+
/*
|
|
4450
|
+
* Nothing here: this held { shot, width } and was read by nothing at all.
|
|
4451
|
+
*
|
|
4452
|
+
* Left over from a version where the panel kept the picture itself. The name is now the panel's own
|
|
4453
|
+
* elements, a few lines down, and two declarations of it stopped the whole client script from
|
|
4454
|
+
* parsing — which takes every handler on the page with it, so the view menu, the composer and the
|
|
4455
|
+
* panel all stop responding at once. Worth knowing that is the symptom: not one broken control but
|
|
4456
|
+
* all of them.
|
|
4457
|
+
*/
|
|
4450
4458
|
|
|
4451
4459
|
/*
|
|
4452
4460
|
* Typing into the page.
|
|
@@ -4487,6 +4495,19 @@ function flushTyping() {
|
|
|
4487
4495
|
async function pageKey(ev) {
|
|
4488
4496
|
// Left alone: the browser's own shortcuts, and anything with a modifier that is not Shift.
|
|
4489
4497
|
if (ev.metaKey || ev.ctrlKey || ev.altKey) return;
|
|
4498
|
+
/*
|
|
4499
|
+
* Scrolling first, and handled here rather than sent to the page as a keypress.
|
|
4500
|
+
*
|
|
4501
|
+
* Pressing PageDown in the page would work too, but only when something scrollable has focus
|
|
4502
|
+
* there — click a text field first and PageDown moves the caret instead, which is the page
|
|
4503
|
+
* behaving correctly and the panel appearing not to. Scrolling the window is unambiguous.
|
|
4504
|
+
*/
|
|
4505
|
+
const scroll = SCROLL_KEYS[ev.key];
|
|
4506
|
+
if (scroll) {
|
|
4507
|
+
ev.preventDefault();
|
|
4508
|
+
scroll();
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4490
4511
|
const special = SPECIAL_KEYS[ev.key];
|
|
4491
4512
|
if (special) {
|
|
4492
4513
|
ev.preventDefault();
|
|
@@ -4516,92 +4537,215 @@ function pageCoords(img, ev) {
|
|
|
4516
4537
|
};
|
|
4517
4538
|
}
|
|
4518
4539
|
|
|
4519
|
-
|
|
4540
|
+
/**
|
|
4541
|
+
* The preview's own elements, built once and kept.
|
|
4542
|
+
*
|
|
4543
|
+
* It used to empty the panel and build a fresh <img> on every update — so every scroll, click and
|
|
4544
|
+
* refresh blanked the picture, decoded a new one, and put it back. That is the blinking: not a slow
|
|
4545
|
+
* page but a panel demolishing itself thirty times a minute. Reported as "no smooth scrolling, like
|
|
4546
|
+
* opening sections", which is exactly what a sequence of rebuilt stills looks like.
|
|
4547
|
+
*
|
|
4548
|
+
* Held here rather than found by id each time, because the handlers are wired once and must read
|
|
4549
|
+
* whatever is current rather than closing over one draw's data.
|
|
4550
|
+
*/
|
|
4551
|
+
const preview = { stage: null, img: null, cursor: null, hint: null, chips: null,
|
|
4552
|
+
controls: [], swapping: false, next: null };
|
|
4553
|
+
|
|
4554
|
+
/**
|
|
4555
|
+
* The pointer, where something just happened.
|
|
4556
|
+
*
|
|
4557
|
+
* Asked for as "see the mouse pointer like a human when operating" — and it is not decoration: a
|
|
4558
|
+
* click whose target you cannot see is a click you cannot check. It moves to the point, presses,
|
|
4559
|
+
* and fades, so watching the panel tells you where the agent went and whether it hit the thing it
|
|
4560
|
+
* meant to.
|
|
4561
|
+
*/
|
|
4562
|
+
function showPointer(pageX, pageY, kind) {
|
|
4563
|
+
const img = preview.img;
|
|
4564
|
+
const cursor = preview.cursor;
|
|
4565
|
+
if (!img || !cursor || !img.naturalWidth) return;
|
|
4566
|
+
const box = img.getBoundingClientRect();
|
|
4567
|
+
const stage = preview.stage.getBoundingClientRect();
|
|
4568
|
+
const scale = box.width / img.naturalWidth;
|
|
4569
|
+
cursor.style.left = Math.round(box.left - stage.left + pageX * scale) + 'px';
|
|
4570
|
+
cursor.style.top = Math.round(box.top - stage.top + pageY * scale) + 'px';
|
|
4571
|
+
cursor.hidden = false;
|
|
4572
|
+
// Restarted rather than added to: a second click during the first animation should look like a
|
|
4573
|
+
// second click, not like nothing.
|
|
4574
|
+
cursor.classList.remove('press', 'move');
|
|
4575
|
+
void cursor.offsetWidth;
|
|
4576
|
+
cursor.classList.add(kind === 'press' ? 'press' : 'move');
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
/** Where a control is on the page, from the last read, so the agent's clicks can be pointed at. */
|
|
4580
|
+
function boxOfControl(want) {
|
|
4581
|
+
const found = (preview.controls || []).find((c) =>
|
|
4582
|
+
(typeof want.ref === 'number' && c.ref === want.ref)
|
|
4583
|
+
|| (want.selector && c.selector === want.selector)
|
|
4584
|
+
|| (want.label && String(c.label || '').toLowerCase() === String(want.label).toLowerCase()));
|
|
4585
|
+
if (!found || !found.box) return null;
|
|
4586
|
+
return { x: found.box.x + found.box.w / 2, y: found.box.y + found.box.h / 2 };
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
/**
|
|
4590
|
+
* Swaps the picture without a blank frame in between.
|
|
4591
|
+
*
|
|
4592
|
+
* decode() waits for the new image to be ready before it is shown, so the old one stays up until
|
|
4593
|
+
* the new one can replace it in a single paint. Without it the src change clears the element and
|
|
4594
|
+
* the panel flashes white between every frame, which is the whole of the flicker.
|
|
4595
|
+
*
|
|
4596
|
+
* Coalesced, because pictures can arrive faster than they decode: while one is being prepared the
|
|
4597
|
+
* newest arrival is held, and anything older than it is dropped — showing a stale frame after a
|
|
4598
|
+
* fresh one is worse than skipping it.
|
|
4599
|
+
*/
|
|
4600
|
+
async function swapShot(src) {
|
|
4601
|
+
if (preview.swapping) { preview.next = src; return; }
|
|
4602
|
+
preview.swapping = true;
|
|
4603
|
+
try {
|
|
4604
|
+
for (let src2 = src; src2; src2 = preview.next, preview.next = null) {
|
|
4605
|
+
const loading = new Image();
|
|
4606
|
+
loading.src = src2;
|
|
4607
|
+
try { if (loading.decode) await loading.decode(); } catch (e) { /* show it regardless */ }
|
|
4608
|
+
if (preview.img) preview.img.src = src2;
|
|
4609
|
+
}
|
|
4610
|
+
} finally {
|
|
4611
|
+
preview.swapping = false;
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
/** The panel's furniture, wired once. */
|
|
4616
|
+
function previewShell() {
|
|
4617
|
+
if (preview.stage && preview.stage.parentNode) return;
|
|
4520
4618
|
const host = $('pvbody');
|
|
4521
4619
|
host.innerHTML = '';
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|| 'Nothing open yet.'));
|
|
4525
|
-
$('pvinfo').textContent = '';
|
|
4526
|
-
return;
|
|
4527
|
-
}
|
|
4528
|
-
$('pvurl').value = data.url || $('pvurl').value;
|
|
4529
|
-
$('pvinfo').textContent = data.title || '';
|
|
4530
|
-
// Shown as unusable rather than silently doing nothing, which is the difference between a
|
|
4531
|
-
// control that is off and one that is broken.
|
|
4532
|
-
$('pvback').disabled = !(data.nav && data.nav.back);
|
|
4533
|
-
$('pvfwd').disabled = !(data.nav && data.nav.forward);
|
|
4534
|
-
if (!data.shot) {
|
|
4535
|
-
host.appendChild(el('div', 'pvnone', 'The browser is open but produced no picture.'));
|
|
4536
|
-
return;
|
|
4537
|
-
}
|
|
4620
|
+
|
|
4621
|
+
const stage = el('div', 'pvstage');
|
|
4538
4622
|
const img = document.createElement('img');
|
|
4539
4623
|
img.className = 'pvshot';
|
|
4540
|
-
img.
|
|
4541
|
-
|
|
4624
|
+
img.alt = 'the page';
|
|
4625
|
+
/*
|
|
4626
|
+
* Focusable, so the keyboard has somewhere to go.
|
|
4627
|
+
*
|
|
4628
|
+
* An image cannot receive key events without this, which was the mechanical reason typing did
|
|
4629
|
+
* nothing: there was no element for the keys to arrive at.
|
|
4630
|
+
*/
|
|
4631
|
+
img.tabIndex = 0;
|
|
4632
|
+
const cursor = el('div', 'pvcursor');
|
|
4633
|
+
cursor.hidden = true;
|
|
4634
|
+
stage.appendChild(img);
|
|
4635
|
+
stage.appendChild(cursor);
|
|
4636
|
+
host.appendChild(stage);
|
|
4637
|
+
|
|
4638
|
+
const hint = el('div', 'pvhint');
|
|
4639
|
+
host.appendChild(hint);
|
|
4640
|
+
const chips = el('div', 'pvctl');
|
|
4641
|
+
host.appendChild(chips);
|
|
4642
|
+
|
|
4643
|
+
/*
|
|
4644
|
+
* Buttons for the same thing the keys do.
|
|
4645
|
+
*
|
|
4646
|
+
* They moved a flat 600 pixels, which is most of a page on a laptop and a third of one on a tall
|
|
4647
|
+
* window — so "scroll down" twice could skip a band of the page or barely move. A page is the
|
|
4648
|
+
* picture's height, whatever that is.
|
|
4649
|
+
*/
|
|
4650
|
+
const scroller = el('div', 'pvhint');
|
|
4651
|
+
for (const [label, go] of [['↑ page', () => scrollByPages(-1)], ['↓ page', () => scrollByPages(1)],
|
|
4652
|
+
['top', () => SCROLL_KEYS.Home()], ['end', () => SCROLL_KEYS.End()]]) {
|
|
4653
|
+
const b = el('button', 'pvbtn', label);
|
|
4654
|
+
b.style.margin = '0 4px';
|
|
4655
|
+
b.onclick = () => { if (preview.img) preview.img.focus(); go(); };
|
|
4656
|
+
scroller.appendChild(b);
|
|
4657
|
+
}
|
|
4658
|
+
scroller.appendChild(el('span', 'dim', ' \u2014 or the wheel, PageUp/PageDown, Home/End'));
|
|
4659
|
+
host.appendChild(scroller);
|
|
4660
|
+
|
|
4542
4661
|
/*
|
|
4543
4662
|
* The cursor answers the question a browser answers: is there something here to click?
|
|
4544
4663
|
*
|
|
4545
|
-
*
|
|
4546
|
-
*
|
|
4547
|
-
*
|
|
4548
|
-
* and was discarding.
|
|
4664
|
+
* A crosshair over the whole page is the language of a screenshot tool. The controls carry their
|
|
4665
|
+
* boxes, so hovering one shows a hand and its label and everywhere else shows an arrow — the same
|
|
4666
|
+
* two states a real page has, from information the panel already had.
|
|
4549
4667
|
*/
|
|
4550
|
-
const controls = (data.controls || []).filter((c) => c.box);
|
|
4551
4668
|
img.onmousemove = (ev) => {
|
|
4552
4669
|
const at = pageCoords(img, ev);
|
|
4553
|
-
const over = controls.find((c) =>
|
|
4670
|
+
const over = (preview.controls || []).filter((c) => c.box).find((c) =>
|
|
4554
4671
|
at.x >= c.box.x && at.x <= c.box.x + c.box.w &&
|
|
4555
4672
|
at.y >= c.box.y && at.y <= c.box.y + c.box.h);
|
|
4556
4673
|
img.style.cursor = over ? 'pointer' : 'default';
|
|
4557
4674
|
img.title = over ? (over.label || over.tag) : '';
|
|
4558
4675
|
};
|
|
4559
4676
|
img.onmouseleave = () => { img.style.cursor = 'default'; img.title = ''; };
|
|
4560
|
-
/*
|
|
4561
|
-
* Focusable, so the keyboard has somewhere to go.
|
|
4562
|
-
*
|
|
4563
|
-
* An image cannot receive key events without this, which is the mechanical reason typing did
|
|
4564
|
-
* nothing: there was no element for the keys to arrive at. Clicking the page focuses it, so
|
|
4565
|
-
* clicking a field and then typing works the way it reads.
|
|
4566
|
-
*/
|
|
4567
|
-
img.tabIndex = 0;
|
|
4568
4677
|
img.onkeydown = (ev) => { void pageKey(ev); };
|
|
4569
4678
|
img.onblur = () => { void flushTyping(); };
|
|
4570
4679
|
|
|
4571
|
-
// The picture is 1280 wide whatever it is displayed at, so a click
|
|
4572
|
-
//
|
|
4680
|
+
// The picture is 1280 wide whatever it is displayed at, so a click is scaled back into page
|
|
4681
|
+
// coordinates or it lands somewhere else entirely.
|
|
4573
4682
|
img.onclick = async (ev) => {
|
|
4574
4683
|
img.focus();
|
|
4575
4684
|
const at = pageCoords(img, ev);
|
|
4685
|
+
showPointer(at.x, at.y, 'press');
|
|
4576
4686
|
$('pvinfo').textContent = 'clicking ' + at.x + ',' + at.y + '…';
|
|
4577
4687
|
await browserDo({ action: 'click', x: at.x, y: at.y });
|
|
4578
4688
|
};
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4689
|
+
|
|
4690
|
+
preview.stage = stage;
|
|
4691
|
+
preview.img = img;
|
|
4692
|
+
preview.cursor = cursor;
|
|
4693
|
+
preview.hint = hint;
|
|
4694
|
+
preview.chips = chips;
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
function drawPreview(data) {
|
|
4698
|
+
const host = $('pvbody');
|
|
4699
|
+
if (!data || data.running === false) {
|
|
4700
|
+
host.innerHTML = '';
|
|
4701
|
+
preview.stage = null;
|
|
4702
|
+
host.appendChild(el('div', 'pvnone', (data && data.note) || 'Nothing open yet.'));
|
|
4703
|
+
$('pvinfo').textContent = '';
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
$('pvurl').value = data.url || $('pvurl').value;
|
|
4707
|
+
$('pvinfo').textContent = data.title || '';
|
|
4708
|
+
// Shown as unusable rather than silently doing nothing, which is the difference between a
|
|
4709
|
+
// control that is off and one that is broken.
|
|
4710
|
+
$('pvback').disabled = !(data.nav && data.nav.back);
|
|
4711
|
+
$('pvfwd').disabled = !(data.nav && data.nav.forward);
|
|
4712
|
+
if (!data.shot) {
|
|
4713
|
+
host.innerHTML = '';
|
|
4714
|
+
preview.stage = null;
|
|
4715
|
+
host.appendChild(el('div', 'pvnone', 'The browser is open but produced no picture.'));
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4719
|
+
previewShell();
|
|
4720
|
+
preview.controls = data.controls || [];
|
|
4721
|
+
void swapShot(data.shot);
|
|
4722
|
+
preview.hint.textContent = 'Click the page to click it for real, then type. '
|
|
4723
|
+
+ preview.controls.length + ' interactive elements.';
|
|
4724
|
+
|
|
4725
|
+
/*
|
|
4726
|
+
* The chips, rebuilt only when they have changed.
|
|
4727
|
+
*
|
|
4728
|
+
* They are the one part that genuinely differs between pages, and rebuilding them on every scroll
|
|
4729
|
+
* — when the controls are identical — was throwing away the element under somebody's pointer
|
|
4730
|
+
* mid-click.
|
|
4731
|
+
*/
|
|
4732
|
+
const want = preview.controls.slice(0, 18)
|
|
4733
|
+
.map((c) => c.ref + '. ' + (c.label || c.tag)).join('|');
|
|
4734
|
+
if (preview.chips.dataset.shape !== want) {
|
|
4735
|
+
preview.chips.dataset.shape = want;
|
|
4736
|
+
preview.chips.innerHTML = '';
|
|
4737
|
+
for (const c of preview.controls.slice(0, 18)) {
|
|
4589
4738
|
const b = el('button', 'pvchip', c.ref + '. ' + (c.label || c.tag));
|
|
4590
4739
|
b.type = 'button';
|
|
4591
4740
|
b.title = c.selector;
|
|
4592
|
-
b.onclick = () =>
|
|
4593
|
-
|
|
4741
|
+
b.onclick = () => {
|
|
4742
|
+
const at = boxOfControl({ ref: c.ref });
|
|
4743
|
+
if (at) showPointer(at.x, at.y, 'press');
|
|
4744
|
+
void browserDo({ action: 'click', ref: c.ref });
|
|
4745
|
+
};
|
|
4746
|
+
preview.chips.appendChild(b);
|
|
4594
4747
|
}
|
|
4595
|
-
host.appendChild(row);
|
|
4596
|
-
}
|
|
4597
|
-
const row = el('div', 'pvhint');
|
|
4598
|
-
for (const [label, by] of [['scroll up', -600], ['scroll down', 600]]) {
|
|
4599
|
-
const b = el('button', 'pvbtn', label);
|
|
4600
|
-
b.style.margin = '0 4px';
|
|
4601
|
-
b.onclick = () => void browserDo({ action: 'scroll', by: by });
|
|
4602
|
-
row.appendChild(b);
|
|
4603
4748
|
}
|
|
4604
|
-
host.appendChild(row);
|
|
4605
4749
|
}
|
|
4606
4750
|
|
|
4607
4751
|
/*
|
|
@@ -4622,16 +4766,69 @@ function drawPreview(data) {
|
|
|
4622
4766
|
$('pvbody').addEventListener('wheel', (ev) => {
|
|
4623
4767
|
if (!document.querySelector('#pvbody .pvshot')) return; // nothing open to scroll
|
|
4624
4768
|
ev.preventDefault();
|
|
4625
|
-
|
|
4626
|
-
if (scrolling.timer) return;
|
|
4627
|
-
scrolling.timer = setTimeout(() => {
|
|
4628
|
-
const by = Math.round(scrolling.by);
|
|
4629
|
-
scrolling.by = 0;
|
|
4630
|
-
scrolling.timer = null;
|
|
4631
|
-
if (by !== 0) void browserDo({ action: 'scroll', by: by });
|
|
4632
|
-
}, SCROLL_BATCH_MS);
|
|
4769
|
+
queueScroll(ev.deltaY);
|
|
4633
4770
|
}, { passive: false });
|
|
4634
4771
|
|
|
4772
|
+
/*
|
|
4773
|
+
* A click anywhere in the panel gives the page the keyboard.
|
|
4774
|
+
*
|
|
4775
|
+
* Only the picture is focusable, so Page Down after clicking the space beside it did nothing —
|
|
4776
|
+
* focus was on the document body and the panel's keys never fired. A browser viewport does not work
|
|
4777
|
+
* that way: clicking near the page is clicking the page. Buttons and fields are left alone, or
|
|
4778
|
+
* pressing one would take the focus straight back off it again.
|
|
4779
|
+
*/
|
|
4780
|
+
$('pvbody').addEventListener('mousedown', (ev) => {
|
|
4781
|
+
if (!preview.img) return;
|
|
4782
|
+
if (ev.target.closest('button, input, select, textarea, a')) return;
|
|
4783
|
+
preview.img.focus();
|
|
4784
|
+
});
|
|
4785
|
+
|
|
4786
|
+
/** Gathers a burst, sends it as one movement. */
|
|
4787
|
+
function queueScroll(by) {
|
|
4788
|
+
scrolling.by += by;
|
|
4789
|
+
if (scrolling.timer) return;
|
|
4790
|
+
scrolling.timer = setTimeout(flushScroll, SCROLL_BATCH_MS);
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4793
|
+
function flushScroll() {
|
|
4794
|
+
const by = Math.round(scrolling.by);
|
|
4795
|
+
scrolling.by = 0;
|
|
4796
|
+
if (scrolling.timer) clearTimeout(scrolling.timer);
|
|
4797
|
+
scrolling.timer = null;
|
|
4798
|
+
if (by !== 0) void browserDo({ action: 'scroll', by: by });
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
/*
|
|
4802
|
+
* Page Down, and the rest of the keys that move a page rather than a few lines.
|
|
4803
|
+
*
|
|
4804
|
+
* A wheel is for reading something already on screen. Getting to the bottom of a long form with one
|
|
4805
|
+
* is thirty gestures, each costing a screenshot, and the panel is not the place to do that. Page Up
|
|
4806
|
+
* and Page Down move a viewport at a time; Home and End go to the ends, expressed as a scroll far
|
|
4807
|
+
* larger than any page because scrollBy clamps and asking for a million pixels lands at the edge.
|
|
4808
|
+
*
|
|
4809
|
+
* A viewport is the picture's own height: the shot is the viewport and nothing beyond it, so its
|
|
4810
|
+
* natural height is exactly how far a page goes. Less a small overlap, so the line you stopped
|
|
4811
|
+
* reading on is still there after the jump — the reason every document reader does this.
|
|
4812
|
+
*
|
|
4813
|
+
* Sent immediately rather than batched. A held Page Down is a handful of events, not fifty, and
|
|
4814
|
+
* waiting on a deliberate keypress reads as the key not working.
|
|
4815
|
+
*/
|
|
4816
|
+
const PAGE_OVERLAP = 60;
|
|
4817
|
+
|
|
4818
|
+
function scrollByPages(pages) {
|
|
4819
|
+
const img = preview.img;
|
|
4820
|
+
const viewport = img && img.naturalHeight ? img.naturalHeight : 720;
|
|
4821
|
+
scrolling.by += pages * Math.max(120, viewport - PAGE_OVERLAP);
|
|
4822
|
+
flushScroll();
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4825
|
+
const SCROLL_KEYS = {
|
|
4826
|
+
PageDown: () => scrollByPages(1),
|
|
4827
|
+
PageUp: () => scrollByPages(-1),
|
|
4828
|
+
Home: () => { scrolling.by -= 1e7; flushScroll(); },
|
|
4829
|
+
End: () => { scrolling.by += 1e7; flushScroll(); },
|
|
4830
|
+
};
|
|
4831
|
+
|
|
4635
4832
|
async function browserDo(body) {
|
|
4636
4833
|
$('pvinfo').textContent = 'working…';
|
|
4637
4834
|
let out;
|
|
@@ -5037,6 +5234,28 @@ function watchTool(e, done) {
|
|
|
5037
5234
|
if (e.name === 'browser') {
|
|
5038
5235
|
$('watch').hidden = false;
|
|
5039
5236
|
if (t.url) $('pvurl').value = t.url;
|
|
5237
|
+
/*
|
|
5238
|
+
* The pointer goes where the agent is acting, before the picture catches up.
|
|
5239
|
+
*
|
|
5240
|
+
* Watching a screenshot change is watching the aftermath; watching the pointer arrive and press
|
|
5241
|
+
* is watching the work. The coordinates come either straight from the call or from the box of
|
|
5242
|
+
* the control it named in the last read, which is how a click by ref or label can be pointed at
|
|
5243
|
+
* rather than merely reported.
|
|
5244
|
+
*/
|
|
5245
|
+
if (!done) {
|
|
5246
|
+
let asked = null;
|
|
5247
|
+
try { asked = e.args ? JSON.parse(e.args) : null; } catch (err) { asked = null; }
|
|
5248
|
+
if (asked && /^(click|type|fill)$/.test(String(asked.action ?? ''))) {
|
|
5249
|
+
const at = typeof asked.x === 'number' && typeof asked.y === 'number'
|
|
5250
|
+
? { x: asked.x, y: asked.y }
|
|
5251
|
+
: boxOfControl({ ref: asked.ref, label: asked.label, selector: asked.selector })
|
|
5252
|
+
// A fill names its fields rather than one target: point at the first it can place.
|
|
5253
|
+
?? (Array.isArray(asked.fields)
|
|
5254
|
+
? asked.fields.map((f) => boxOfControl(f)).find(Boolean) ?? null
|
|
5255
|
+
: null);
|
|
5256
|
+
if (at) showPointer(at.x, at.y, asked.action === 'click' ? 'press' : 'move');
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5040
5259
|
watchHead(verb, t.url || 'the page');
|
|
5041
5260
|
trailPush({ label: t.url ? shortUrl(t.url) : 'page', verb, kind: 'page',
|
|
5042
5261
|
url: t.url || '', ok: done ? e.ok !== false : undefined });
|
|
@@ -5581,7 +5800,9 @@ addEventListener('keydown', (ev) => {
|
|
|
5581
5800
|
* already in use — and since the box also filtered, 476 models were immediately narrowed to the
|
|
5582
5801
|
* one you were on. It looked exactly like a text field with no choice in it, because it was.
|
|
5583
5802
|
*/
|
|
5584
|
-
const catalog = { provider: null, entries: [], listed: false, note: '', chosen: ''
|
|
5803
|
+
const catalog = { provider: null, entries: [], listed: false, note: '', chosen: '',
|
|
5804
|
+
/* Only the newest ask may land. See loadModels. */ seq: 0,
|
|
5805
|
+
/* What each provider last said, so re-selecting one is instant. */ seen: {} };
|
|
5585
5806
|
|
|
5586
5807
|
/** Fills the provider dropdown from whatever the registry holds, declared entries included. */
|
|
5587
5808
|
/** The providers as last listed, so the key row knows what the chosen one needs. */
|
|
@@ -5738,7 +5959,7 @@ $('keydrop').onclick = async () => {
|
|
|
5738
5959
|
$('keysaid').style.color = 'var(--muted)';
|
|
5739
5960
|
$('keysaid').textContent = 'Forgotten. ' + name + ' will ask for a key again.';
|
|
5740
5961
|
await fillProviders(name);
|
|
5741
|
-
|
|
5962
|
+
forgetCatalogs();
|
|
5742
5963
|
};
|
|
5743
5964
|
|
|
5744
5965
|
$('keygo').onclick = async () => {
|
|
@@ -5769,7 +5990,7 @@ $('keygo').onclick = async () => {
|
|
|
5769
5990
|
// The listing now reports it present, so the row goes away and the models can be asked again.
|
|
5770
5991
|
keyReplacing = '';
|
|
5771
5992
|
await fillProviders($('mprov').value);
|
|
5772
|
-
|
|
5993
|
+
forgetCatalogs();
|
|
5773
5994
|
void loadModels($('mprov').value, state.model === '-' ? '' : state.model);
|
|
5774
5995
|
};
|
|
5775
5996
|
|
|
@@ -5800,8 +6021,7 @@ function drawModels() {
|
|
|
5800
6021
|
catalog.chosen = '';
|
|
5801
6022
|
return;
|
|
5802
6023
|
}
|
|
5803
|
-
|
|
5804
|
-
+ (catalog.chosen ? ' · using ' + catalog.chosen : ' · click one to choose');
|
|
6024
|
+
modelNote(shown.length, total);
|
|
5805
6025
|
|
|
5806
6026
|
// Grouped by vendor, which is what turns a flat several-hundred-entry list into something you
|
|
5807
6027
|
// can actually read.
|
|
@@ -5813,31 +6033,142 @@ function drawModels() {
|
|
|
5813
6033
|
}
|
|
5814
6034
|
const on = m.id === catalog.chosen;
|
|
5815
6035
|
const row = el('div', 'mrow' + (on ? ' on' : ''));
|
|
6036
|
+
row.dataset.id = m.id;
|
|
5816
6037
|
row.appendChild(el('div', 'mid', m.id));
|
|
5817
6038
|
row.appendChild(el('div', 'mdet', m.detail));
|
|
5818
|
-
row.onclick = () =>
|
|
6039
|
+
row.onclick = () => chooseModel(m.id);
|
|
5819
6040
|
host.appendChild(row);
|
|
5820
6041
|
if (on && !first) first = row;
|
|
5821
6042
|
}
|
|
5822
6043
|
if (shown.length > 400) {
|
|
5823
6044
|
host.appendChild(el('div', 'mgroup', 'and ' + (shown.length - 400) + ' more — keep typing'));
|
|
5824
6045
|
}
|
|
5825
|
-
// The model in use should be in view when the dialog opens, not hundreds of rows down.
|
|
6046
|
+
// The model in use should be in view when the dialog opens, not hundreds of rows down. Only on a
|
|
6047
|
+
// redraw: doing it when somebody picks a row scrolls the list out from under the click.
|
|
5826
6048
|
if (first) first.scrollIntoView({ block: 'center' });
|
|
5827
6049
|
}
|
|
5828
6050
|
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
6051
|
+
/*
|
|
6052
|
+
* How many are listed, which one Switch will use, and how to ask again.
|
|
6053
|
+
*
|
|
6054
|
+
* The list is held for ten minutes, which is right for a provider whose catalogue changes weekly
|
|
6055
|
+
* and wrong for the machine under the desk: pull a model into Ollama and it would not appear until
|
|
6056
|
+
* the cache expired, with nothing on screen to suggest otherwise. So the count line carries the way
|
|
6057
|
+
* to re-ask — and it goes past both the dialog's memory and the server's.
|
|
6058
|
+
*/
|
|
6059
|
+
function modelNote(showing, total) {
|
|
6060
|
+
const note = $('mnote');
|
|
6061
|
+
note.textContent = (showing === total ? total + ' models' : showing + ' of ' + total)
|
|
6062
|
+
+ (catalog.chosen ? ' · using ' + catalog.chosen : ' · click one to choose') + ' · ';
|
|
6063
|
+
const again = el('button', 'linkish', 'ask again');
|
|
6064
|
+
again.type = 'button';
|
|
6065
|
+
again.title = 'Ask ' + catalog.provider + ' what it serves now, ignoring what it said before';
|
|
6066
|
+
again.onclick = () => {
|
|
6067
|
+
const who = $('mprov').value;
|
|
6068
|
+
note.textContent = 'Asking ' + who + ' what it serves…';
|
|
6069
|
+
void loadModels(who, catalog.chosen, { fresh: true });
|
|
6070
|
+
};
|
|
6071
|
+
note.appendChild(again);
|
|
6072
|
+
}
|
|
6073
|
+
|
|
6074
|
+
/*
|
|
6075
|
+
* Picking one, without redrawing the list.
|
|
6076
|
+
*
|
|
6077
|
+
* This used to set the choice and call drawModels, which empties the list and builds it again:
|
|
6078
|
+
* measured at 400 rows, 50ms of rebuilding to change which row is highlighted — and then
|
|
6079
|
+
* scrollIntoView on the newly built row moved the list by 8,304 pixels, so the list jumped out from
|
|
6080
|
+
* under the click that had just been made. Nothing about a selection needs any of that.
|
|
6081
|
+
*
|
|
6082
|
+
* One class off, one class on, and the count line updated.
|
|
6083
|
+
*/
|
|
6084
|
+
function chooseModel(id) {
|
|
6085
|
+
catalog.chosen = id;
|
|
6086
|
+
const host = $('mlist');
|
|
6087
|
+
const was = host.querySelector('.mrow.on');
|
|
6088
|
+
if (was) was.classList.remove('on');
|
|
6089
|
+
const now = host.querySelector('.mrow[data-id="' + cssEscape(id) + '"]');
|
|
6090
|
+
if (now) now.classList.add('on');
|
|
6091
|
+
const showing = host.querySelectorAll('.mrow').length;
|
|
6092
|
+
modelNote(showing, catalog.entries.length);
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
/*
|
|
6096
|
+
* A model id inside an attribute selector.
|
|
6097
|
+
*
|
|
6098
|
+
* Ids carry slashes, dots and colons — "anthropic/claude-sonnet-4.5" — and a dot in a selector is a
|
|
6099
|
+
* class. CSS.escape where it exists, which is everywhere current, and a conservative fallback
|
|
6100
|
+
* rather than an unescaped selector that would throw.
|
|
6101
|
+
*/
|
|
6102
|
+
function cssEscape(value) {
|
|
6103
|
+
if (window.CSS && CSS.escape) return CSS.escape(value);
|
|
6104
|
+
return String(value).replace(/["\\]/g, '\\$&');
|
|
6105
|
+
}
|
|
6106
|
+
|
|
6107
|
+
/*
|
|
6108
|
+
* Throws away what the providers said, so the next ask is live.
|
|
6109
|
+
*
|
|
6110
|
+
* Every "re-ask" used to be catalog.provider = null on its own. That still reads as re-asking, and
|
|
6111
|
+
* once answers were kept per provider it silently stopped being true: the kept answer would be
|
|
6112
|
+
* served instead, so entering a key was followed by the same empty list the provider gave before it
|
|
6113
|
+
* had one — indistinguishable from the key not working.
|
|
6114
|
+
*/
|
|
6115
|
+
function forgetCatalogs() {
|
|
6116
|
+
catalog.provider = null;
|
|
6117
|
+
catalog.seen = {};
|
|
6118
|
+
}
|
|
6119
|
+
|
|
6120
|
+
/*
|
|
6121
|
+
* Asks the selected provider what it serves.
|
|
6122
|
+
*
|
|
6123
|
+
* ## Only the newest ask may land
|
|
6124
|
+
*
|
|
6125
|
+
* Two of these can be in flight at once — pick a provider, then pick another before the first has
|
|
6126
|
+
* answered — and there was nothing to stop the slower one arriving last and overwriting the faster.
|
|
6127
|
+
* Measured with a provider taking 2.6 seconds and one taking 120ms: selecting the slow one and then
|
|
6128
|
+
* the quick one left the menu reading "fastco" above a list of the *other* provider's models. Click
|
|
6129
|
+
* one of those rows and you have chosen a model the selected provider does not serve.
|
|
6130
|
+
*
|
|
6131
|
+
* Worse, it wedged: catalog.provider was then the provider you were no longer on, so selecting it
|
|
6132
|
+
* again matched the "already loaded" check and did nothing at all. Closing the dialog and opening
|
|
6133
|
+
* it again cleared that, which is precisely what people were doing to get out of it.
|
|
6134
|
+
*
|
|
6135
|
+
* A sequence number, checked after every await. A stale answer is dropped rather than drawn.
|
|
6136
|
+
*
|
|
6137
|
+
* ## Asked once per provider
|
|
6138
|
+
*
|
|
6139
|
+
* Answers are kept per provider for as long as the dialog session lasts, so going back to one you
|
|
6140
|
+
* have already looked at is immediate rather than another round trip. The server holds them for ten
|
|
6141
|
+
* minutes as well, so even a fresh page load usually answers from memory.
|
|
6142
|
+
*/
|
|
6143
|
+
async function loadModels(provider, preselect, opts) {
|
|
6144
|
+
const seq = ++catalog.seq;
|
|
6145
|
+
const fresh = !!(opts && opts.fresh);
|
|
6146
|
+
const settle = (out) => {
|
|
6147
|
+
catalog.provider = provider;
|
|
6148
|
+
catalog.entries = out.entries || [];
|
|
6149
|
+
catalog.listed = !!out.listed;
|
|
6150
|
+
catalog.note = out.note || '';
|
|
6151
|
+
// Preselect only if the provider actually serves it: carrying a name across providers is how a
|
|
6152
|
+
// session ends up asking Anthropic for qwen3-coder.
|
|
6153
|
+
catalog.chosen = preselect && catalog.entries.some((m) => m.id === preselect) ? preselect : '';
|
|
6154
|
+
if (!catalog.listed && preselect) $('mmodel').value = preselect;
|
|
6155
|
+
drawModels();
|
|
6156
|
+
};
|
|
6157
|
+
|
|
6158
|
+
const known = fresh ? null : catalog.seen[provider];
|
|
6159
|
+
if (known) { settle(known); return; }
|
|
6160
|
+
|
|
5832
6161
|
$('mlist').innerHTML = '';
|
|
5833
6162
|
$('mnote').textContent = 'Asking ' + provider + ' what it serves…';
|
|
5834
6163
|
let out;
|
|
5835
6164
|
// Named, so a key typed into this session can be used to ask what the provider serves.
|
|
5836
6165
|
try {
|
|
5837
6166
|
out = await api('/api/models?provider=' + encodeURIComponent(provider)
|
|
5838
|
-
+ (state.session ? '&session=' + encodeURIComponent(state.session) : '')
|
|
6167
|
+
+ (state.session ? '&session=' + encodeURIComponent(state.session) : '')
|
|
6168
|
+
+ (fresh ? '&fresh=1' : ''));
|
|
5839
6169
|
}
|
|
5840
6170
|
catch (e) {
|
|
6171
|
+
if (seq !== catalog.seq) return; // a newer selection has been made
|
|
5841
6172
|
catalog.provider = provider; catalog.entries = []; catalog.listed = false;
|
|
5842
6173
|
catalog.note = e.message;
|
|
5843
6174
|
catalog.chosen = '';
|
|
@@ -5846,15 +6177,11 @@ async function loadModels(provider, preselect) {
|
|
|
5846
6177
|
drawModels();
|
|
5847
6178
|
return;
|
|
5848
6179
|
}
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
catalog.
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
// session ends up asking Anthropic for qwen3-coder.
|
|
5855
|
-
catalog.chosen = preselect && catalog.entries.some((m) => m.id === preselect) ? preselect : '';
|
|
5856
|
-
if (!catalog.listed && preselect) $('mmodel').value = preselect;
|
|
5857
|
-
drawModels();
|
|
6180
|
+
// Remembered even if it is no longer wanted: it is a true answer about that provider, and the
|
|
6181
|
+
// person may well select it next.
|
|
6182
|
+
catalog.seen[provider] = out;
|
|
6183
|
+
if (seq !== catalog.seq) return;
|
|
6184
|
+
settle(out);
|
|
5858
6185
|
}
|
|
5859
6186
|
|
|
5860
6187
|
$('pmodel').onclick = async () => {
|
|
@@ -5872,7 +6199,7 @@ $('pmodel').onclick = async () => {
|
|
|
5872
6199
|
* anything.
|
|
5873
6200
|
*/
|
|
5874
6201
|
$('mmodel').value = '';
|
|
5875
|
-
|
|
6202
|
+
forgetCatalogs(); // re-ask: a key may have been set since last time
|
|
5876
6203
|
$('mdlg').showModal();
|
|
5877
6204
|
|
|
5878
6205
|
const info = await fillProviders(state.provider);
|
|
@@ -5987,7 +6314,7 @@ async function afterProviderSaved(out) {
|
|
|
5987
6314
|
$('provkey').placeholder = keyEnvPlaceholder('');
|
|
5988
6315
|
await fillProviders(out.name);
|
|
5989
6316
|
$('mmodel').value = out.defaultModel === 'default' ? '' : out.defaultModel;
|
|
5990
|
-
|
|
6317
|
+
forgetCatalogs();
|
|
5991
6318
|
void loadModels(out.name);
|
|
5992
6319
|
}
|
|
5993
6320
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuxMlB,CAAC;AACF,CAAC"}
|