@aria-framework/ai 0.25.0 → 0.26.1
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/README.md +60 -2
- package/browser/ai-panels.js +138 -138
- package/index.js +352 -309
- package/lmxStatus.js +1 -1
- package/lmxVerify.js +402 -402
- package/package.json +2 -2
- package/providers/anthropic.js +11 -4
- package/providers/lmx.js +52 -12
- package/providers/openai-compatible.js +37 -10
- package/views/ai/job-card.ejs +182 -182
- package/views/ai/lmx-stack.ejs +426 -426
package/README.md
CHANGED
|
@@ -32,7 +32,11 @@ const ai = createAiClient({
|
|
|
32
32
|
logger: console // optional
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
const r = await ai.complete({
|
|
35
|
+
const r = await ai.complete({
|
|
36
|
+
system, messages, maxTokens: 400,
|
|
37
|
+
schema, // optional: constrained JSON
|
|
38
|
+
// reasoning: 'full', // optional, lmx only: let the model think fully (see Reasoning below)
|
|
39
|
+
});
|
|
36
40
|
// r.text, r.json (when schema), r.model, r.usage.total, r.ms
|
|
37
41
|
```
|
|
38
42
|
|
|
@@ -40,6 +44,12 @@ Every failure is thrown as an `AiError` with a `kind`: `disabled`, `unconfigured
|
|
|
40
44
|
`timeout`, `auth`, `rate_limit`, `bad_response` or `refused`. The `message` is written for
|
|
41
45
|
an admin screen and never includes a URL or key.
|
|
42
46
|
|
|
47
|
+
**A failed call that spent tokens is still metered** (0.26.1). When the model answered but the answer
|
|
48
|
+
was unusable (it thought until the limit, or a structured reply was cut off), the error carries
|
|
49
|
+
`usage` (`{ prompt, completion, total }`) and `budget.record(cfg, { usage, model, ms, failed: true }, meta)`
|
|
50
|
+
is called before the error is thrown. A failure that spent nothing (refused, unreachable, timeout)
|
|
51
|
+
records nothing. If `record` throws, the original error still reaches the caller.
|
|
52
|
+
|
|
43
53
|
`providerStore`, `usageStore`, `speedStore` and `lmxStore` are optional db-worker-backed
|
|
44
54
|
stores. Each exports a `schemaFor(dialect)`, so the app's migration can be checked against it.
|
|
45
55
|
|
|
@@ -59,7 +69,7 @@ to write yourself:
|
|
|
59
69
|
| Engine URLs come from the document | Resolved on every call, and never stored |
|
|
60
70
|
| A status outage is not an inference outage | Keeps routing on the last good document for 3 minutes (`staleMs`), logging loudly |
|
|
61
71
|
| Pin the certificate; never disable verification | Uses an undici `Agent({ connect: { ca } })` per stack. Never `NODE_EXTRA_CA_CERTS`, never `rejectUnauthorized:false` |
|
|
62
|
-
| Choose the reasoning flag from the model | `qwen` → `chat_template_kwargs.enable_thinking=false`, `gpt-oss` → `reasoning_effort:'low'`, anything else gets neither (with a warning) |
|
|
72
|
+
| Choose the reasoning flag from the model | By default thinking is kept to a minimum: `qwen` → `chat_template_kwargs.enable_thinking=false`, `gpt-oss` → `reasoning_effort:'low'`, anything else gets neither (with a warning). `reasoning: 'full'` on the call (since 0.26.0) sends `enable_thinking=true` / `reasoning_effort:'high'` instead. The flag always overrides a raw `extra`; only the named option changes it. See [Reasoning](#reasoning-reasoning-full) |
|
|
63
73
|
| Size requests against `maxInputTokens` (per slot) | `contextTokens` is taken from the engine, not from config |
|
|
64
74
|
| Identity is `(instance, id)`, falling back to `name` | `lmx.engineId` is matched first; `rekeyPlan()` migrates rows when ids are minted or engines renamed |
|
|
65
75
|
| A `429` is about the key, not the engine | Fails fast (since 0.25.0): throws `rate_limit` with `lmxSkip: 'lmx_throttled'` and `retryAfterMs`. The client never waits; whether to wait or move on is the caller's decision |
|
|
@@ -166,6 +176,54 @@ instead:
|
|
|
166
176
|
Failover across several engines for one job is the app's job; lmx provides none. List
|
|
167
177
|
endpoints in preference order and take the first one that serves.
|
|
168
178
|
|
|
179
|
+
### Reasoning (`reasoning: 'full'`)
|
|
180
|
+
|
|
181
|
+
By default the adapter makes the model think as little as its family allows, so a quick answer
|
|
182
|
+
stays quick. Pass `reasoning: 'full'` on a `complete()` call to let the model think fully:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
const r = await ai.complete({
|
|
186
|
+
system, messages, schema,
|
|
187
|
+
reasoning: 'full',
|
|
188
|
+
maxTokens: 4096, // at least 4096; see the trap below
|
|
189
|
+
}); // and give the endpoint timeoutMs: 120000 or more
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**When to use it.** Use it for judgement tasks where the answer depends on weighing evidence,
|
|
193
|
+
such as alert triage. Do not use it for quick summaries, rewrites or extraction. They do not get
|
|
194
|
+
better, only slower.
|
|
195
|
+
|
|
196
|
+
**What it costs.** It is much slower, roughly 30–45 s per answer on lab hardware instead of a few
|
|
197
|
+
seconds, and it spends many more tokens, because the thinking is generated and billed like any
|
|
198
|
+
other output. Budget for both.
|
|
199
|
+
|
|
200
|
+
**The trap.** Thinking comes out of `maxTokens`. If the limit is too small, the model spends all
|
|
201
|
+
of it thinking and returns **an empty answer, with no error from the server** (`finish_reason:
|
|
202
|
+
"length"`). The adapter turns that, and a reply cut off inside an unclosed `<think>` block, into
|
|
203
|
+
a `bad_response` AiError saying it ran out of room (and, when the server says so, that the
|
|
204
|
+
tokens went on internal reasoning), but the work is lost either way. Use **`maxTokens` ≥ 4096** and a
|
|
205
|
+
**`timeoutMs` ≥ 120 s** (120000) on the endpoint, or the deadline cuts the answer off first.
|
|
206
|
+
|
|
207
|
+
**What is sent, per model family** (read from the model the engine is running on each call):
|
|
208
|
+
|
|
209
|
+
| Family | Default (no `reasoning`) | `reasoning: 'full'` |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| qwen | `{"chat_template_kwargs":{"enable_thinking":false}}` | `{"chat_template_kwargs":{"enable_thinking":true}}` |
|
|
212
|
+
| gpt-oss | `{"reasoning_effort":"low"}` | `{"reasoning_effort":"high"}` |
|
|
213
|
+
| anything else | nothing (warning logged) | nothing (warning logged that `'full'` could not be honoured) |
|
|
214
|
+
|
|
215
|
+
Rules:
|
|
216
|
+
|
|
217
|
+
- `'full'` is the only value. Leaving the option out (or `null`) means the default. Any other value,
|
|
218
|
+
for example `'high'`, `true` or `''`, is refused with `AiError` kind `refused` before any
|
|
219
|
+
request is made, whichever provider the endpoint uses.
|
|
220
|
+
- The flag is merged **after** `extra`, so a raw `extra: { chat_template_kwargs: … }` or
|
|
221
|
+
`extra: { reasoning_effort: … }` cannot change it. Only `reasoning` can.
|
|
222
|
+
- Only `lmx` honours it. On `openai-compatible`, `lmstudio` or `anthropic` the client logs a
|
|
223
|
+
warning, removes the option, and nothing reaches that provider's request body.
|
|
224
|
+
- `REASONING_MODES` (package root) lists the accepted values for an app that validates its own
|
|
225
|
+
settings.
|
|
226
|
+
|
|
169
227
|
### Embeddings
|
|
170
228
|
|
|
171
229
|
Call `client.embed(cfg, texts, { signal })` and pass your own resolved embedding config; it never
|
package/browser/ai-panels.js
CHANGED
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fold on the Inference list, and prefilling the supervisor form from a panel.
|
|
3
|
-
*
|
|
4
|
-
* WHY ANYTHING FOLDS. A stable stack is four engines, three credentials, a certificate fingerprint
|
|
5
|
-
* and a verification report — worth having, not worth reading every time you open this page to do
|
|
6
|
-
* something else. Folded, a panel is one line that still carries the whole verdict: status, the
|
|
7
|
-
* count of what depends on it, all three credentials and when it was last checked. Folding hides
|
|
8
|
-
* detail; it must never hide the reason you would have opened it.
|
|
9
|
-
*
|
|
10
|
-
* A PANEL WITH A PROBLEM IGNORES WHAT YOU REMEMBERED. `data-panel-attention` is stamped by the
|
|
11
|
-
* server on anything with a missing engine, an expiring certificate, a failed check or a nearly
|
|
12
|
-
* spent cap. Those open and stay open. Attention beats tidiness — the alternative is a page that
|
|
13
|
-
* quietly honours a fold you chose last week and hides the thing that broke yesterday.
|
|
14
|
-
*
|
|
15
|
-
* THE PREFERENCE IS PER BROWSER AND DISPOSABLE. It is a convenience about how a page looks to one
|
|
16
|
-
* person, so localStorage is the right home and losing it costs nothing. Every access is guarded:
|
|
17
|
-
* a browser set to block site data throws on read, and a settings page must not break because
|
|
18
|
-
* somebody tightened their privacy settings.
|
|
19
|
-
*/
|
|
20
|
-
(function () {
|
|
21
|
-
'use strict';
|
|
22
|
-
|
|
23
|
-
var KEY = 's101.ai.panels';
|
|
24
|
-
|
|
25
|
-
function readPrefs() {
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(window.localStorage.getItem(KEY) || '{}') || {};
|
|
28
|
-
} catch (e) {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writePref(id, open) {
|
|
34
|
-
try {
|
|
35
|
-
var prefs = readPrefs();
|
|
36
|
-
prefs[id] = !!open;
|
|
37
|
-
window.localStorage.setItem(KEY, JSON.stringify(prefs));
|
|
38
|
-
} catch (e) { /* private window, or site data blocked — the fold still works for this visit */ }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function bodyOf(panel) {
|
|
42
|
-
var btn = panel.querySelector('[data-panel-toggle]');
|
|
43
|
-
if (!btn) return null;
|
|
44
|
-
return document.getElementById(btn.getAttribute('aria-controls'));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function setOpen(panel, open, remember) {
|
|
48
|
-
var btn = panel.querySelector('[data-panel-toggle]');
|
|
49
|
-
var body = bodyOf(panel);
|
|
50
|
-
if (!btn || !body) return;
|
|
51
|
-
// `hidden`, not a style: the server renders the closed state the same way, so a panel does not
|
|
52
|
-
// flicker open on load before this script runs.
|
|
53
|
-
body.hidden = !open;
|
|
54
|
-
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
55
|
-
panel.classList.toggle('panel-open', open);
|
|
56
|
-
if (remember) writePref(panel.getAttribute('data-panel'), open);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function restore() {
|
|
60
|
-
var prefs = readPrefs();
|
|
61
|
-
var panels = document.querySelectorAll('[data-panel]');
|
|
62
|
-
for (var i = 0; i < panels.length; i += 1) {
|
|
63
|
-
var panel = panels[i];
|
|
64
|
-
var id = panel.getAttribute('data-panel');
|
|
65
|
-
// The server already opened this one and means it. Do not consult the preference at all —
|
|
66
|
-
// reading it and then ignoring it is the same thing, but invites somebody to "fix" it later.
|
|
67
|
-
if (panel.hasAttribute('data-panel-attention')) {
|
|
68
|
-
setOpen(panel, true, false);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (Object.prototype.hasOwnProperty.call(prefs, id)) setOpen(panel, !!prefs[id], false);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
document.addEventListener('click', function (ev) {
|
|
76
|
-
var toggle = ev.target.closest('[data-panel-toggle]');
|
|
77
|
-
if (toggle) {
|
|
78
|
-
var panel = toggle.closest('[data-panel]');
|
|
79
|
-
if (!panel) return;
|
|
80
|
-
var body = bodyOf(panel);
|
|
81
|
-
setOpen(panel, !!(body && body.hidden), true);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── EDITING A STACK HAPPENS IN THE PANEL ────────────────────────────────────────────────────
|
|
86
|
-
//
|
|
87
|
-
// It used to happen in a form at the foot of the page, and Edit scrolled you down to it — away
|
|
88
|
-
// from the stack you were reading, into a form that also served as "Add a supervisor" and said
|
|
89
|
-
// nothing about which stack it had been filled with. Nothing needs prefilling now: every panel
|
|
90
|
-
// renders its own form, from the server, with its own values already in it.
|
|
91
|
-
var open = ev.target.closest('[data-stack-edit-open]');
|
|
92
|
-
if (open) { stackMode(open.closest('[data-stack]'), true); return; }
|
|
93
|
-
|
|
94
|
-
var cancel = ev.target.closest('[data-stack-cancel]');
|
|
95
|
-
if (cancel) {
|
|
96
|
-
var panel2 = cancel.closest('[data-stack]');
|
|
97
|
-
// A NEW stack has no reading half to go back to, so cancelling it puts the blank panel away.
|
|
98
|
-
if (panel2 && panel2.hasAttribute('data-stack-new')) { panel2.hidden = true; return; }
|
|
99
|
-
// Otherwise reload rather than restore: a Cancel that left half-typed values behind, ready to
|
|
100
|
-
// be posted by the next Save, would be worse than the extra request.
|
|
101
|
-
window.location.reload();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// "Add LMX" reveals the blank panel that is already on the page, at the top of the list.
|
|
106
|
-
var add = ev.target.closest('[data-stack-add]');
|
|
107
|
-
if (add) {
|
|
108
|
-
var blank = document.querySelector('[data-stack-new]');
|
|
109
|
-
if (!blank) return;
|
|
110
|
-
blank.hidden = false;
|
|
111
|
-
blank.scrollIntoView({ block: 'nearest' });
|
|
112
|
-
var id = blank.querySelector('[name="id"]');
|
|
113
|
-
if (id) id.focus();
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
/** Swap one stack panel between reading and editing. */
|
|
118
|
-
function stackMode(panel, editing) {
|
|
119
|
-
if (!panel) return;
|
|
120
|
-
var view = panel.querySelector('[data-stack-view]');
|
|
121
|
-
var form = panel.querySelector('[data-stack-edit]');
|
|
122
|
-
if (!view || !form) return;
|
|
123
|
-
view.hidden = editing;
|
|
124
|
-
form.hidden = !editing;
|
|
125
|
-
if (editing) {
|
|
126
|
-
// The id is readonly on an existing stack, so focus the first field somebody can actually
|
|
127
|
-
// change rather than one that will not accept a keystroke.
|
|
128
|
-
var first = form.querySelector('[name="label"]') || form.querySelector('[name="status_url"]');
|
|
129
|
-
if (first) first.focus();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (document.readyState === 'loading') {
|
|
134
|
-
document.addEventListener('DOMContentLoaded', restore);
|
|
135
|
-
} else {
|
|
136
|
-
restore();
|
|
137
|
-
}
|
|
138
|
-
})();
|
|
1
|
+
/**
|
|
2
|
+
* The fold on the Inference list, and prefilling the supervisor form from a panel.
|
|
3
|
+
*
|
|
4
|
+
* WHY ANYTHING FOLDS. A stable stack is four engines, three credentials, a certificate fingerprint
|
|
5
|
+
* and a verification report — worth having, not worth reading every time you open this page to do
|
|
6
|
+
* something else. Folded, a panel is one line that still carries the whole verdict: status, the
|
|
7
|
+
* count of what depends on it, all three credentials and when it was last checked. Folding hides
|
|
8
|
+
* detail; it must never hide the reason you would have opened it.
|
|
9
|
+
*
|
|
10
|
+
* A PANEL WITH A PROBLEM IGNORES WHAT YOU REMEMBERED. `data-panel-attention` is stamped by the
|
|
11
|
+
* server on anything with a missing engine, an expiring certificate, a failed check or a nearly
|
|
12
|
+
* spent cap. Those open and stay open. Attention beats tidiness — the alternative is a page that
|
|
13
|
+
* quietly honours a fold you chose last week and hides the thing that broke yesterday.
|
|
14
|
+
*
|
|
15
|
+
* THE PREFERENCE IS PER BROWSER AND DISPOSABLE. It is a convenience about how a page looks to one
|
|
16
|
+
* person, so localStorage is the right home and losing it costs nothing. Every access is guarded:
|
|
17
|
+
* a browser set to block site data throws on read, and a settings page must not break because
|
|
18
|
+
* somebody tightened their privacy settings.
|
|
19
|
+
*/
|
|
20
|
+
(function () {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
var KEY = 's101.ai.panels';
|
|
24
|
+
|
|
25
|
+
function readPrefs() {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(window.localStorage.getItem(KEY) || '{}') || {};
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writePref(id, open) {
|
|
34
|
+
try {
|
|
35
|
+
var prefs = readPrefs();
|
|
36
|
+
prefs[id] = !!open;
|
|
37
|
+
window.localStorage.setItem(KEY, JSON.stringify(prefs));
|
|
38
|
+
} catch (e) { /* private window, or site data blocked — the fold still works for this visit */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bodyOf(panel) {
|
|
42
|
+
var btn = panel.querySelector('[data-panel-toggle]');
|
|
43
|
+
if (!btn) return null;
|
|
44
|
+
return document.getElementById(btn.getAttribute('aria-controls'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setOpen(panel, open, remember) {
|
|
48
|
+
var btn = panel.querySelector('[data-panel-toggle]');
|
|
49
|
+
var body = bodyOf(panel);
|
|
50
|
+
if (!btn || !body) return;
|
|
51
|
+
// `hidden`, not a style: the server renders the closed state the same way, so a panel does not
|
|
52
|
+
// flicker open on load before this script runs.
|
|
53
|
+
body.hidden = !open;
|
|
54
|
+
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
55
|
+
panel.classList.toggle('panel-open', open);
|
|
56
|
+
if (remember) writePref(panel.getAttribute('data-panel'), open);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function restore() {
|
|
60
|
+
var prefs = readPrefs();
|
|
61
|
+
var panels = document.querySelectorAll('[data-panel]');
|
|
62
|
+
for (var i = 0; i < panels.length; i += 1) {
|
|
63
|
+
var panel = panels[i];
|
|
64
|
+
var id = panel.getAttribute('data-panel');
|
|
65
|
+
// The server already opened this one and means it. Do not consult the preference at all —
|
|
66
|
+
// reading it and then ignoring it is the same thing, but invites somebody to "fix" it later.
|
|
67
|
+
if (panel.hasAttribute('data-panel-attention')) {
|
|
68
|
+
setOpen(panel, true, false);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(prefs, id)) setOpen(panel, !!prefs[id], false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
document.addEventListener('click', function (ev) {
|
|
76
|
+
var toggle = ev.target.closest('[data-panel-toggle]');
|
|
77
|
+
if (toggle) {
|
|
78
|
+
var panel = toggle.closest('[data-panel]');
|
|
79
|
+
if (!panel) return;
|
|
80
|
+
var body = bodyOf(panel);
|
|
81
|
+
setOpen(panel, !!(body && body.hidden), true);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── EDITING A STACK HAPPENS IN THE PANEL ────────────────────────────────────────────────────
|
|
86
|
+
//
|
|
87
|
+
// It used to happen in a form at the foot of the page, and Edit scrolled you down to it — away
|
|
88
|
+
// from the stack you were reading, into a form that also served as "Add a supervisor" and said
|
|
89
|
+
// nothing about which stack it had been filled with. Nothing needs prefilling now: every panel
|
|
90
|
+
// renders its own form, from the server, with its own values already in it.
|
|
91
|
+
var open = ev.target.closest('[data-stack-edit-open]');
|
|
92
|
+
if (open) { stackMode(open.closest('[data-stack]'), true); return; }
|
|
93
|
+
|
|
94
|
+
var cancel = ev.target.closest('[data-stack-cancel]');
|
|
95
|
+
if (cancel) {
|
|
96
|
+
var panel2 = cancel.closest('[data-stack]');
|
|
97
|
+
// A NEW stack has no reading half to go back to, so cancelling it puts the blank panel away.
|
|
98
|
+
if (panel2 && panel2.hasAttribute('data-stack-new')) { panel2.hidden = true; return; }
|
|
99
|
+
// Otherwise reload rather than restore: a Cancel that left half-typed values behind, ready to
|
|
100
|
+
// be posted by the next Save, would be worse than the extra request.
|
|
101
|
+
window.location.reload();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// "Add LMX" reveals the blank panel that is already on the page, at the top of the list.
|
|
106
|
+
var add = ev.target.closest('[data-stack-add]');
|
|
107
|
+
if (add) {
|
|
108
|
+
var blank = document.querySelector('[data-stack-new]');
|
|
109
|
+
if (!blank) return;
|
|
110
|
+
blank.hidden = false;
|
|
111
|
+
blank.scrollIntoView({ block: 'nearest' });
|
|
112
|
+
var id = blank.querySelector('[name="id"]');
|
|
113
|
+
if (id) id.focus();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** Swap one stack panel between reading and editing. */
|
|
118
|
+
function stackMode(panel, editing) {
|
|
119
|
+
if (!panel) return;
|
|
120
|
+
var view = panel.querySelector('[data-stack-view]');
|
|
121
|
+
var form = panel.querySelector('[data-stack-edit]');
|
|
122
|
+
if (!view || !form) return;
|
|
123
|
+
view.hidden = editing;
|
|
124
|
+
form.hidden = !editing;
|
|
125
|
+
if (editing) {
|
|
126
|
+
// The id is readonly on an existing stack, so focus the first field somebody can actually
|
|
127
|
+
// change rather than one that will not accept a keystroke.
|
|
128
|
+
var first = form.querySelector('[name="label"]') || form.querySelector('[name="status_url"]');
|
|
129
|
+
if (first) first.focus();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (document.readyState === 'loading') {
|
|
134
|
+
document.addEventListener('DOMContentLoaded', restore);
|
|
135
|
+
} else {
|
|
136
|
+
restore();
|
|
137
|
+
}
|
|
138
|
+
})();
|