@arraypress/extension-kit 1.5.0 → 1.7.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/README.md +120 -4
- package/package.json +2 -2
- package/src/ai/model.d.ts +317 -0
- package/src/ai/model.js +312 -0
- package/src/build/vendor.js +1 -1
- package/src/content/slices.d.ts +63 -0
- package/src/content/slices.js +80 -0
- package/src/content/watch.d.ts +71 -0
- package/src/content/watch.js +95 -0
- package/src/index.d.ts +35 -4
- package/src/index.js +25 -4
- package/src/scripting/registration.d.ts +36 -0
- package/src/scripting/registration.js +54 -0
- package/src/styles/chrome-ui.css +11 -0
- package/src/testing/downloads.d.ts +41 -0
- package/src/testing/downloads.js +70 -0
- package/src/testing/e2e.d.ts +15 -0
- package/src/testing/e2e.js +26 -1
- package/src/testing/fake-model.d.ts +87 -0
- package/src/testing/fake-model.js +142 -0
- package/src/testing/fake-storage.d.ts +2 -0
- package/src/testing/fake-storage.js +15 -3
- package/src/testing/index.d.ts +19 -5
- package/src/testing/index.js +14 -5
- package/src/testing/live.d.ts +146 -0
- package/src/testing/live.js +151 -0
- package/src/testing/messages.d.ts +33 -0
- package/src/testing/messages.js +51 -0
- package/src/testing/server.d.ts +23 -3
- package/src/testing/server.js +36 -6
- package/src/{testing → time}/pool.d.ts +4 -3
- package/src/{testing → time}/pool.js +4 -3
- package/src/ui/model-row.d.ts +111 -0
- package/src/ui/model-row.js +108 -0
- package/src/ui/text-dialog.d.ts +44 -7
- package/src/ui/text-dialog.js +54 -16
package/README.md
CHANGED
|
@@ -14,6 +14,11 @@ The pieces ArrayPress's Chrome extensions share, so each is written once:
|
|
|
14
14
|
("Rename folder", or several fields as "Add site search"), the toast with Undo and a settings card's site list, built on the
|
|
15
15
|
stylesheet's classes; they work in a content script's shadow root too.
|
|
16
16
|
- **Sites**: what people type into a per-site list, and whether a page is on one.
|
|
17
|
+
- **Chrome's on-device model** (Gemini Nano, the Prompt API): availability, a download from the
|
|
18
|
+
person's click with progress, one base session with a `clone()` per question, a time limit on
|
|
19
|
+
every answer, cold starts, and the settings page's “Chrome’s built-in AI” row.
|
|
20
|
+
- **Content-script helpers**: a big page worked through in slices that never hold it long, and a
|
|
21
|
+
change watcher for single-page apps and endless lists.
|
|
17
22
|
- **Build and test helpers** (Node): icon rendering, vendoring npm packages (with a staleness
|
|
18
23
|
check), test builds, store screenshots, zipping for the Chrome Web Store, a fake
|
|
19
24
|
`chrome.storage`, Playwright helpers with time limits, packaging checks, and a copied-code guard.
|
|
@@ -193,6 +198,99 @@ withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com'], what a list
|
|
|
193
198
|
A site is http or https with a dot in its host (or `localhost`); text without a scheme is https;
|
|
194
199
|
`localhost:8080` is a host and port. Pure, so a content script can load `sites/sites.js` alone.
|
|
195
200
|
|
|
201
|
+
## A content script on every site
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
// The content script runs everywhere only while the optional all-sites grant is held, less the
|
|
205
|
+
// "Never on" sites; one call brings the registration in line, whatever changed.
|
|
206
|
+
const sync = contentScriptSync(chrome.scripting, 'prices', async () => {
|
|
207
|
+
const settings = await store.load(chrome.storage);
|
|
208
|
+
if (!settings.everySite || !(await chrome.permissions.contains({ origins: ALL_SITES }))) return null;
|
|
209
|
+
return { js: ['content/loader.js'], matches: ALL_SITES, excludeMatches: settings.neverSites.flatMap(hostPatterns), runAt: 'document_idle' };
|
|
210
|
+
});
|
|
211
|
+
chrome.permissions.onAdded.addListener(() => sync());
|
|
212
|
+
chrome.permissions.onRemoved.addListener(() => sync()); // site access taken back on chrome://extensions
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Registers, updates or unregisters the script `id` to match what `want()` gives at the time (null:
|
|
216
|
+
none). Calls run one after another, so a quick run of changes can't register twice or end out of
|
|
217
|
+
order; a refused change rejects that call only.
|
|
218
|
+
|
|
219
|
+
## Chrome's on-device model
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
import { chromeLanguageModel, createModelSession } from '../vendor/extension-kit/index.js';
|
|
223
|
+
|
|
224
|
+
const ENGLISH = { expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }] };
|
|
225
|
+
const model = createModelSession({
|
|
226
|
+
model: chromeLanguageModel, // read when used: Chrome may add it after start-up
|
|
227
|
+
options: ENGLISH, // availability() is asked with what create() gets
|
|
228
|
+
initialPrompts: [{ role: 'system', content: 'You name downloaded files.' }, ...EXAMPLE],
|
|
229
|
+
});
|
|
230
|
+
const answer = await model.ask(prompt, { timeoutMs: 2500, signal, responseConstraint: schema });
|
|
231
|
+
if (answer.text === null) useRules(answer.reason); // 'unavailable' | 'late' | 'aborted' | 'failed'
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Following https://developer.chrome.com/docs/ai/prompt-api: only an `available` model is used (a
|
|
235
|
+
question never downloads it); one base session is made when first needed and kept; each question
|
|
236
|
+
goes to a `clone()` of it, let go after; the answer's `AbortSignal` fires at the time limit. The
|
|
237
|
+
first session after Chrome starts can take ~18 s while the model loads: a question with a shorter
|
|
238
|
+
limit comes back `'late'`, and the session goes on loading for the next one. Nothing loads it ahead
|
|
239
|
+
of time (that would load the model on every start). `release()` lets the base session go.
|
|
240
|
+
|
|
241
|
+
`modelAvailability(api, options)` never throws (`'missing'` without a Prompt API).
|
|
242
|
+
`downloadModel(api, options, {onProgress})` is the only `create()` that may download: call it
|
|
243
|
+
first in a click's handler, with nothing awaited before it. The settings row:
|
|
244
|
+
|
|
245
|
+
```html
|
|
246
|
+
<div class="row two-line">
|
|
247
|
+
<span class="label">
|
|
248
|
+
<span data-i18n="optionsModel">Chrome’s built-in AI</span>
|
|
249
|
+
<span class="secondary"><span id="model-status"></span> <span id="model-help" hidden></span></span>
|
|
250
|
+
<progress id="model-progress" max="1" value="0" hidden></progress>
|
|
251
|
+
</span>
|
|
252
|
+
<button id="model-download" type="button" data-i18n="optionsModelDownload" hidden>Download</button>
|
|
253
|
+
</div>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
const row = createModelRow(
|
|
258
|
+
{ status: byId('model-status'), help: byId('model-help'), download: byId('model-download', HTMLButtonElement), progress: byId('model-progress', HTMLProgressElement) },
|
|
259
|
+
{ options: ENGLISH, state: () => ask(MESSAGES.modelState), t, onShown: ({ state }) => { useAi.disabled = state === 'off'; } },
|
|
260
|
+
);
|
|
261
|
+
await row.refresh();
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Its words are the page's messages under `MODEL_MESSAGES`' keys (`optionsModel`, `optionsModelReady`,
|
|
265
|
+
`optionsModelNeedsDownload`, `optionsModelDownloadHelp`, `optionsModelDownload`,
|
|
266
|
+
`optionsModelDownloading` with `$PERCENT$`, `optionsModelDownloadFailed`, `optionsModelUnsupported`,
|
|
267
|
+
`optionsModelRequirements`, `optionsModelMissing`, `optionsModelMissingHelp`); `describeModel` maps
|
|
268
|
+
a state to them. Tests use `fakeLanguageModel` (`/testing`), the Prompt API's shape: availability,
|
|
269
|
+
download progress (or a failed download), a cold start, slow and failed answers, aborts, and a log
|
|
270
|
+
of what it was asked.
|
|
271
|
+
|
|
272
|
+
## Content scripts
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
import { inSlices } from '../vendor/extension-kit/content/slices.js';
|
|
276
|
+
import { watchChanges } from '../vendor/extension-kit/content/watch.js';
|
|
277
|
+
|
|
278
|
+
await inSlices(document.body, (block) => scanner.scan(block)); // ≤4,000 characters a block, 40 ms slices
|
|
279
|
+
const stop = watchChanges(document.body, {
|
|
280
|
+
changed: (nodes) => nodes.forEach((node) => scanner.scan(node)), // one batch, each subtree once
|
|
281
|
+
removed: (node) => scanner.forget(node), // at once
|
|
282
|
+
ignore: (node) => node instanceof Element && node.localName === 'my-note', // the extension's own
|
|
283
|
+
settleMs: 120, maxWaitMs: 600,
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`inSlices(root, work, {blockChars, sliceMs, pause})` cuts a page into blocks (`blocksOf`) and
|
|
288
|
+
hands the main thread back whenever a slice has run its time, so a 2,000-item listing never
|
|
289
|
+
freezes. `watchChanges` hands over what was added or changed a little after the page stops
|
|
290
|
+
changing (at most `maxWaitMs` while it keeps on); an element with a few children (`regroupChildren`,
|
|
291
|
+
default 6) whose children changed is handed over whole. Both are pure DOM, so a content script
|
|
292
|
+
imports them directly (list them in `web_accessible_resources`).
|
|
293
|
+
|
|
196
294
|
## Time limits
|
|
197
295
|
|
|
198
296
|
```js
|
|
@@ -203,6 +301,13 @@ const groups = await within(chrome.tabGroups.query({}), 2000, []); // a Chrome c
|
|
|
203
301
|
rejects; its timer cleared once the work settles (the work itself isn't stopped). In the runtime
|
|
204
302
|
half, so a service worker or page can use it; the test helpers use it too.
|
|
205
303
|
|
|
304
|
+
```js
|
|
305
|
+
await eachLimited(files, 4, async (file) => save(file)); // at most four at once
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`eachLimited(items, limit, work)`: `work(item, slot, index)` on every item, at most `limit` at once;
|
|
309
|
+
in the runtime half too (`/testing` exports it as well).
|
|
310
|
+
|
|
206
311
|
## Page components
|
|
207
312
|
|
|
208
313
|
Chrome's own components, made in the page by script (no markup to copy), styled by `chrome-ui.css`.
|
|
@@ -248,7 +353,11 @@ const site = await askText({
|
|
|
248
353
|
});
|
|
249
354
|
```
|
|
250
355
|
|
|
251
|
-
Resolves to the text, trimmed, or `null` when cancelled.
|
|
356
|
+
Resolves to the text, trimmed, or `null` when cancelled. A field can be `multiline` (a textarea,
|
|
357
|
+
`rows` high: Enter starts a new line), carry a `hint` under it (or a function of its text, kept up
|
|
358
|
+
as it's typed: “12 words”), and take a `value` that's still being worked out (a promise, say an
|
|
359
|
+
on-device model's suggestion): the field waits, disabled, with `waiting` under it, then takes the
|
|
360
|
+
text and the focus (unless the person has typed meanwhile); a failed one leaves it empty. The action button (default "Save") stays
|
|
252
361
|
disabled while the text is blank or `validate` gives a reason, which shows under the field
|
|
253
362
|
(`checkText` is that rule, pure). Enter presses the action button; Esc and Cancel cancel; the
|
|
254
363
|
focus goes back to what had it, or to `returnTo` (an element, or a function asked at the end).
|
|
@@ -364,21 +473,27 @@ except those listed, each with a reason:
|
|
|
364
473
|
|
|
365
474
|
| Export | What it does |
|
|
366
475
|
|---|---|
|
|
367
|
-
| `
|
|
476
|
+
| `fakeLanguageModel(state)` | A fake Prompt API (`LanguageModel`): `availability`, `create` (download progress, a cold start, `failDownload`), sessions that `clone`, answer (`answerMs`, `answer(prompt)`, `failPrompt`) and stop on their signal; `log` of what it was asked, `set` to change it. Browser-safe, so a test build can put it on `globalThis`. |
|
|
477
|
+
| `fakeChromeStorage({local, sync, session, now})` | In-memory `chrome.storage`: JSON copies, `onChanged` for real changes only, sync's 8,192-byte item quota, its 102,400 bytes in all (`SYNC_QUOTA_BYTES`) and 120 writes a minute. `settled()` waits for listeners. |
|
|
368
478
|
| `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
|
|
479
|
+
| `launchDownloading(chromium, dir, options)`, `filesIn(dir)` | An extension in a browser that downloads for real: `{…launchExtension's, downloads}`, the profile's own Downloads folder (Playwright otherwise saves downloads under made-up names no extension sees); the files under a folder, downloads still running left out. |
|
|
369
480
|
| `extensionProfile(chromium, dir, options)` | A profile to start the browser on more than once (a restart, a first start with a changed manifest): `{dir, launch(), remove()}`. |
|
|
481
|
+
| `runChecks(runner, body, cleanUps)` | A script's checks: an error outside any check fails, the clean-ups always run (each within 30 s), then the tally. |
|
|
482
|
+
| `messageLookup(extensionDir, locale)`, `readMessages(extensionDir, locale)` | `chrome.i18n.getMessage` over the real `_locales` (a locale over en, named placeholders filled), for unit tests. |
|
|
370
483
|
| `checkRunner({timeout})` | Named checks that report and go on: `check(name, fn, {timeout})`, `fail(name)` for an error found outside a check, `finish()`. A check that runs past its limit (`CHECK_MS`, 60 s) fails, and the run goes on. |
|
|
371
|
-
| `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)
|
|
484
|
+
| `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)` (also in the main entry). |
|
|
372
485
|
| `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
|
|
373
486
|
| `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
|
|
374
487
|
| `testBuildDir(root, name)` | As in `/build`, for the tests that load the build. |
|
|
375
|
-
| `serve(routes, {port})` | Serve test pages on 127.0.0.1 from a table, a `Map` or a function of the path; `{origin, port, server, close()}`. |
|
|
488
|
+
| `serve(routes, {port, tls})` | Serve test pages on 127.0.0.1 from a table, a `Map` or a function of the path; `{origin, port, server, close()}`. With `tls` (`testCertificate()`, a throwaway one from `openssl`), over HTTPS: for a site Chrome reaches only over HTTPS, mapped with `--host-resolver-rules`, Chromium started with `--ignore-certificate-errors`. |
|
|
376
489
|
| `until(fn, what, timeout?)` | Poll until truthy. |
|
|
377
490
|
| `offscreenEmulator(cdp)` | Emulate light/dark inside the offscreen document. |
|
|
378
491
|
| `recordIconCalls(worker)` | Record `chrome.action.setIcon` calls Chrome accepted. |
|
|
379
492
|
| `installWarnings(worker, manifest)` | Chrome's own install warnings for a manifest (`chrome.management.getPermissionWarningsByManifest`, no permission needed), from a loaded extension's worker. |
|
|
380
493
|
| `warningsAddedBy(worker, manifest, names)` | `{warnings, without, added}`: the warnings with and without some permissions, and those only they bring (`withoutPermissions` is the pure part). |
|
|
381
494
|
| `savedTabGroups(profile)` | Chrome's saved tab groups (`{title, pinned, urls}`) read from a closed profile's `Sync Data/LevelDB`: no extension API lists them. |
|
|
495
|
+
| `parseLiveOptions(argv, env, {usage})`, `parseProxy(address)` | A live checker's command line (`--only`, `--category`, `--concurrency`, `--proxy` or `HTTPS_PROXY`, `--locale`, `--timezone`, `--out`, `--help`), and a proxy address for Playwright with its password hidden for the output. |
|
|
496
|
+
| `exitPoint(request)` | Where the run's traffic leaves for the internet (`{ip, country, where, source}`), from a keyless lookup service through the context's proxy: sites decide what to show by it. |
|
|
382
497
|
| `filesUnder`, `zipEntries`, `missingReferences`, `manifestReferences` | For a packaging test: the zip is the folder, and every path the extension names resolves (a `#fragment` or `?query` ignored: `referencedPath`). |
|
|
383
498
|
|
|
384
499
|
## Stylesheet classes
|
|
@@ -407,6 +522,7 @@ except those listed, each with a reason:
|
|
|
407
522
|
| Manager layout | `.manager`/`.layout`, `.sidebar` with `.tree` > `.tree-item`, `.main` with `.list-card` |
|
|
408
523
|
| Menu (cr-action-menu) | `<dialog class="menu">` of `button[role="menuitem"]`, `hr` (or `createMenu`) |
|
|
409
524
|
| ⋮ icon | `<span class="icon icon-more-vert" aria-hidden="true">` (or `moreActionsButton`) |
|
|
525
|
+
| Subpage header (settings-subpage) | `<div class="subpage-head">` with a back `button.icon-button` holding `.icon.icon-arrow-back`, then `<h2>` |
|
|
410
526
|
| Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (`askText`, `askFields`) |
|
|
411
527
|
| Toast (cr-toast) | `<div class="toast" popover="manual">text <button>Undo</button></div>` (or `createToast`); with `hidden` instead of `popover`, it shows over the page but not the top layer |
|
|
412
528
|
| Drawer (cr-drawer) | `<dialog class="drawer">` with `.drawer-head` and a `.nav` |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arraypress/extension-kit",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Shared pieces for Chrome extensions: Chrome's own page styles, a Chrome Sync settings store, the light/dark toolbar glyph, About links, and build and test helpers.",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "Shared pieces for Chrome extensions: Chrome's own page styles and components, a Chrome Sync settings store, the light/dark toolbar glyph, Chrome's on-device model, content-script helpers, About links, and build and test helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"types": "src/index.d.ts",
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome's on-device model (Gemini Nano, through the Prompt API), the way
|
|
3
|
+
* every extension here uses it, following
|
|
4
|
+
* https://developer.chrome.com/docs/ai/prompt-api:
|
|
5
|
+
*
|
|
6
|
+
* - `availability()` is asked with the options `create()` gets, and only an
|
|
7
|
+
* 'available' model is used: nothing here downloads it, except
|
|
8
|
+
* {@link downloadModel}, which a page calls from the person's click (Chrome
|
|
9
|
+
* only downloads from a user gesture) and which reports progress;
|
|
10
|
+
* - one base session, with the instructions and examples, is made once and
|
|
11
|
+
* kept; each question goes to a `clone()` of it, so answers don't leak into
|
|
12
|
+
* each other;
|
|
13
|
+
* - each answer has a time limit: an `AbortSignal` stops the answer once
|
|
14
|
+
* it's late, and the caller gets a reason instead of a hang;
|
|
15
|
+
* - the first session after Chrome starts can take ~18 s while the model
|
|
16
|
+
* loads (a cold start, M3 Max). A question with a shorter limit gets
|
|
17
|
+
* 'late', and the session goes on loading for the next one. Nothing loads
|
|
18
|
+
* it ahead of time: that would load the model on every start for work that
|
|
19
|
+
* may never come.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here touches `chrome.*` or the global itself: the Prompt API comes
|
|
22
|
+
* in as `model` (read when used, since Chrome may add it after start-up), so
|
|
23
|
+
* tests hand it the kit's fake (`fakeLanguageModel`, `/testing`) at exactly
|
|
24
|
+
* this edge. TypeScript's DOM library doesn't declare the Prompt API yet, so
|
|
25
|
+
* the parts used are typed here.
|
|
26
|
+
*
|
|
27
|
+
* @module ai/model
|
|
28
|
+
*/
|
|
29
|
+
export type Availability = 'available' | 'downloadable' | 'downloading' | 'unavailable' | 'missing';
|
|
30
|
+
export type PromptOptions = {
|
|
31
|
+
/**
|
|
32
|
+
* Stops the answer.
|
|
33
|
+
*/
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
/**
|
|
36
|
+
* A JSON Schema (or RegExp) the answer must follow.
|
|
37
|
+
*/
|
|
38
|
+
responseConstraint?: object;
|
|
39
|
+
};
|
|
40
|
+
export type LanguageModelSession = {
|
|
41
|
+
/**
|
|
42
|
+
* The whole answer.
|
|
43
|
+
*/
|
|
44
|
+
prompt: (prompt: string, options?: PromptOptions) => Promise<string>;
|
|
45
|
+
/**
|
|
46
|
+
* A copy with the same history.
|
|
47
|
+
*/
|
|
48
|
+
clone: (options?: {
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}) => Promise<LanguageModelSession>;
|
|
51
|
+
/**
|
|
52
|
+
* Let it go.
|
|
53
|
+
*/
|
|
54
|
+
destroy: () => void;
|
|
55
|
+
};
|
|
56
|
+
export type InitialPrompt = {
|
|
57
|
+
role: 'system' | 'user' | 'assistant';
|
|
58
|
+
content: string;
|
|
59
|
+
};
|
|
60
|
+
export type LanguageModelOptions = {
|
|
61
|
+
/**
|
|
62
|
+
* What it's given.
|
|
63
|
+
*/
|
|
64
|
+
expectedInputs?: {
|
|
65
|
+
type: string;
|
|
66
|
+
languages: string[];
|
|
67
|
+
}[];
|
|
68
|
+
/**
|
|
69
|
+
* What it answers in.
|
|
70
|
+
*/
|
|
71
|
+
expectedOutputs?: {
|
|
72
|
+
type: string;
|
|
73
|
+
languages: string[];
|
|
74
|
+
}[];
|
|
75
|
+
/**
|
|
76
|
+
* Its instructions and examples.
|
|
77
|
+
*/
|
|
78
|
+
initialPrompts?: InitialPrompt[];
|
|
79
|
+
/**
|
|
80
|
+
* Hears `downloadprogress` (`loaded`, 0–1) while the model downloads.
|
|
81
|
+
*/
|
|
82
|
+
monitor?: (monitor: EventTarget) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Stops creating it.
|
|
85
|
+
*/
|
|
86
|
+
signal?: AbortSignal;
|
|
87
|
+
};
|
|
88
|
+
export type LanguageModelStatic = {
|
|
89
|
+
/**
|
|
90
|
+
* Whether it can run here.
|
|
91
|
+
*/
|
|
92
|
+
availability: (options?: LanguageModelOptions) => Promise<Availability>;
|
|
93
|
+
/**
|
|
94
|
+
* A session (downloads the model first, from a click).
|
|
95
|
+
*/
|
|
96
|
+
create: (options?: LanguageModelOptions) => Promise<LanguageModelSession>;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* The model's answer to `availability()`: 'available', 'downloadable',
|
|
100
|
+
* 'downloading' or 'unavailable'; and, from {@link modelAvailability},
|
|
101
|
+
* 'missing' when this Chrome has no Prompt API.
|
|
102
|
+
*
|
|
103
|
+
* @typedef {'available' | 'downloadable' | 'downloading' | 'unavailable' | 'missing'} Availability
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* @typedef {object} PromptOptions
|
|
107
|
+
* @property {AbortSignal} [signal] Stops the answer.
|
|
108
|
+
* @property {object} [responseConstraint] A JSON Schema (or RegExp) the answer must follow.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* @typedef {object} LanguageModelSession
|
|
112
|
+
* @property {(prompt: string, options?: PromptOptions) => Promise<string>} prompt The whole answer.
|
|
113
|
+
* @property {(options?: {signal?: AbortSignal}) => Promise<LanguageModelSession>} clone A copy with the same history.
|
|
114
|
+
* @property {() => void} destroy Let it go.
|
|
115
|
+
*/
|
|
116
|
+
/**
|
|
117
|
+
* @typedef {{role: 'system' | 'user' | 'assistant', content: string}} InitialPrompt
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* @typedef {object} LanguageModelOptions
|
|
121
|
+
* @property {{type: string, languages: string[]}[]} [expectedInputs] What it's given.
|
|
122
|
+
* @property {{type: string, languages: string[]}[]} [expectedOutputs] What it answers in.
|
|
123
|
+
* @property {InitialPrompt[]} [initialPrompts] Its instructions and examples.
|
|
124
|
+
* @property {(monitor: EventTarget) => void} [monitor] Hears `downloadprogress` (`loaded`, 0–1) while the model downloads.
|
|
125
|
+
* @property {AbortSignal} [signal] Stops creating it.
|
|
126
|
+
*/
|
|
127
|
+
/**
|
|
128
|
+
* @typedef {object} LanguageModelStatic
|
|
129
|
+
* @property {(options?: LanguageModelOptions) => Promise<Availability>} availability Whether it can run here.
|
|
130
|
+
* @property {(options?: LanguageModelOptions) => Promise<LanguageModelSession>} create A session (downloads the model first, from a click).
|
|
131
|
+
*/
|
|
132
|
+
/**
|
|
133
|
+
* Chrome's `LanguageModel`, or undefined when this Chrome has none.
|
|
134
|
+
*
|
|
135
|
+
* @param {object} [scope] - Where to look. Default `globalThis` (a page, or the service worker).
|
|
136
|
+
* @returns {LanguageModelStatic | undefined} The Prompt API.
|
|
137
|
+
* @example
|
|
138
|
+
* const sessions = createModelSession({ model: chromeLanguageModel, options: ENGLISH });
|
|
139
|
+
*/
|
|
140
|
+
export declare function chromeLanguageModel(scope?: object): LanguageModelStatic | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* The model's state, never throwing: 'missing' without a Prompt API, and
|
|
143
|
+
* 'unavailable' when asking fails.
|
|
144
|
+
*
|
|
145
|
+
* @param {LanguageModelStatic | undefined} model - The Prompt API.
|
|
146
|
+
* @param {LanguageModelOptions} options - The options sessions are created with.
|
|
147
|
+
* @returns {Promise<Availability>} Its state.
|
|
148
|
+
* @example
|
|
149
|
+
* await modelAvailability(chromeLanguageModel(), ENGLISH); // 'available'
|
|
150
|
+
*/
|
|
151
|
+
export declare function modelAvailability(model: LanguageModelStatic | undefined, options: LanguageModelOptions): Promise<Availability>;
|
|
152
|
+
/**
|
|
153
|
+
* Download the model, from the person's click: `create()` with a monitor,
|
|
154
|
+
* the session let go once made. Call it first in the click's handler, with
|
|
155
|
+
* nothing awaited before it, or Chrome refuses (no user activation).
|
|
156
|
+
*
|
|
157
|
+
* @param {LanguageModelStatic | undefined} model - The Prompt API.
|
|
158
|
+
* @param {LanguageModelOptions} options - The options sessions are created with.
|
|
159
|
+
* @param {{onProgress?: (loaded: number) => void, signal?: AbortSignal}} [handlers] - `onProgress`: how much is
|
|
160
|
+
* downloaded, 0–1, as Chrome reports it; `signal`: stops it.
|
|
161
|
+
* @returns {Promise<Availability>} The state afterwards ('available' once it's here).
|
|
162
|
+
* @throws {Error} When there's no Prompt API, or Chrome couldn't download it.
|
|
163
|
+
* @example
|
|
164
|
+
* button.addEventListener('click', () => {
|
|
165
|
+
* downloadModel(chromeLanguageModel(), ENGLISH, { onProgress: (loaded) => { bar.value = loaded; } })
|
|
166
|
+
* .then(show, () => show('downloadable', 'The download didn’t finish. Try again.'));
|
|
167
|
+
* });
|
|
168
|
+
*/
|
|
169
|
+
export declare function downloadModel(model: LanguageModelStatic | undefined, options: LanguageModelOptions, { onProgress, signal }?: {
|
|
170
|
+
onProgress?: (loaded: number) => void;
|
|
171
|
+
signal?: AbortSignal;
|
|
172
|
+
}): Promise<Availability>;
|
|
173
|
+
/**
|
|
174
|
+
* The settings page's words for a state: standard message keys (the
|
|
175
|
+
* extension's `_locales` names them alike), as `SITE_LIST_MESSAGES` does
|
|
176
|
+
* for a site list.
|
|
177
|
+
*/
|
|
178
|
+
export declare const MODEL_MESSAGES: Readonly<{
|
|
179
|
+
/** “Chrome’s built-in AI”: the row's name. */
|
|
180
|
+
name: "optionsModel";
|
|
181
|
+
/** “Checking…” before the state is known. */
|
|
182
|
+
checking: "optionsModelChecking";
|
|
183
|
+
/** “Ready”. */
|
|
184
|
+
ready: "optionsModelReady";
|
|
185
|
+
/** “Needs download”. */
|
|
186
|
+
needsDownload: "optionsModelNeedsDownload";
|
|
187
|
+
/** “Chrome downloads its model once and shares it with every site and extension that uses it”. */
|
|
188
|
+
downloadHelp: "optionsModelDownloadHelp";
|
|
189
|
+
/** “Download”: the button. */
|
|
190
|
+
download: "optionsModelDownload";
|
|
191
|
+
/** “Downloading… $PERCENT$%” (content `$1`). */
|
|
192
|
+
downloading: "optionsModelDownloading";
|
|
193
|
+
/** “The download didn’t finish. Try again.” */
|
|
194
|
+
downloadFailed: "optionsModelDownloadFailed";
|
|
195
|
+
/** “Not supported on this computer”. */
|
|
196
|
+
unsupported: "optionsModelUnsupported";
|
|
197
|
+
/** What a computer needs to run it. */
|
|
198
|
+
requirements: "optionsModelRequirements";
|
|
199
|
+
/** “This Chrome doesn’t have it”. */
|
|
200
|
+
missing: "optionsModelMissing";
|
|
201
|
+
/** What to do about that, and that the extension works without it. */
|
|
202
|
+
missingHelp: "optionsModelMissingHelp";
|
|
203
|
+
}>;
|
|
204
|
+
export type ModelStatus = {
|
|
205
|
+
/**
|
|
206
|
+
* Ready to use, can be downloaded, or can't run here.
|
|
207
|
+
*/
|
|
208
|
+
state: 'ready' | 'pending' | 'off';
|
|
209
|
+
/**
|
|
210
|
+
* The status's message key ({@link MODEL_MESSAGES}).
|
|
211
|
+
*/
|
|
212
|
+
status: string;
|
|
213
|
+
/**
|
|
214
|
+
* The help's message key ('' for none).
|
|
215
|
+
*/
|
|
216
|
+
help: string;
|
|
217
|
+
/**
|
|
218
|
+
* Whether to offer Download.
|
|
219
|
+
*/
|
|
220
|
+
canDownload: boolean;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* The model's state, as a settings page shows it.
|
|
224
|
+
*
|
|
225
|
+
* @typedef {object} ModelStatus
|
|
226
|
+
* @property {'ready' | 'pending' | 'off'} state Ready to use, can be downloaded, or can't run here.
|
|
227
|
+
* @property {string} status The status's message key ({@link MODEL_MESSAGES}).
|
|
228
|
+
* @property {string} help The help's message key ('' for none).
|
|
229
|
+
* @property {boolean} canDownload Whether to offer Download.
|
|
230
|
+
*/
|
|
231
|
+
/**
|
|
232
|
+
* Describe an availability for a settings page. Pure.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} availability - 'available', 'downloadable', 'downloading', 'unavailable' or 'missing'.
|
|
235
|
+
* @returns {ModelStatus} How to show it.
|
|
236
|
+
* @example
|
|
237
|
+
* describeModel('downloadable'); // { state: 'pending', status: 'optionsModelNeedsDownload', help: 'optionsModelDownloadHelp', canDownload: true }
|
|
238
|
+
*/
|
|
239
|
+
export declare function describeModel(availability: string): ModelStatus;
|
|
240
|
+
export type ModelAnswer = {
|
|
241
|
+
text: string;
|
|
242
|
+
reason: 'answered';
|
|
243
|
+
} | {
|
|
244
|
+
text: null;
|
|
245
|
+
reason: 'unavailable' | 'late' | 'aborted' | 'failed';
|
|
246
|
+
};
|
|
247
|
+
export type AskOptions = {
|
|
248
|
+
/**
|
|
249
|
+
* The most the answer may take, from the moment it's asked: the base
|
|
250
|
+
* session (if it must be made) and the answer.
|
|
251
|
+
*/
|
|
252
|
+
timeoutMs: number;
|
|
253
|
+
/**
|
|
254
|
+
* Stops it sooner (the person cancelled).
|
|
255
|
+
*/
|
|
256
|
+
signal?: AbortSignal;
|
|
257
|
+
/**
|
|
258
|
+
* A JSON Schema the answer must follow.
|
|
259
|
+
*/
|
|
260
|
+
responseConstraint?: object;
|
|
261
|
+
};
|
|
262
|
+
export type ModelSession = {
|
|
263
|
+
/**
|
|
264
|
+
* The model's state, or 'missing'.
|
|
265
|
+
*/
|
|
266
|
+
availability: () => Promise<Availability>;
|
|
267
|
+
/**
|
|
268
|
+
* Ask a copy of the base session.
|
|
269
|
+
*/
|
|
270
|
+
ask: (prompt: string, options: AskOptions) => Promise<ModelAnswer>;
|
|
271
|
+
/**
|
|
272
|
+
* Let the base session go (the next question makes it again).
|
|
273
|
+
*/
|
|
274
|
+
release: () => void;
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* What a question got: the answer, or why there's none: the model isn't
|
|
278
|
+
* on this computer ('unavailable'), the time ran out first ('late', a cold
|
|
279
|
+
* start among them), the caller's signal stopped it ('aborted'), or the model
|
|
280
|
+
* failed ('failed').
|
|
281
|
+
*
|
|
282
|
+
* @typedef {{text: string, reason: 'answered'} | {text: null, reason: 'unavailable' | 'late' | 'aborted' | 'failed'}} ModelAnswer
|
|
283
|
+
*/
|
|
284
|
+
/**
|
|
285
|
+
* @typedef {object} AskOptions
|
|
286
|
+
* @property {number} timeoutMs The most the answer may take, from the moment it's asked: the base
|
|
287
|
+
* session (if it must be made) and the answer.
|
|
288
|
+
* @property {AbortSignal} [signal] Stops it sooner (the person cancelled).
|
|
289
|
+
* @property {object} [responseConstraint] A JSON Schema the answer must follow.
|
|
290
|
+
*/
|
|
291
|
+
/**
|
|
292
|
+
* @typedef {object} ModelSession
|
|
293
|
+
* @property {() => Promise<Availability>} availability The model's state, or 'missing'.
|
|
294
|
+
* @property {(prompt: string, options: AskOptions) => Promise<ModelAnswer>} ask Ask a copy of the base session.
|
|
295
|
+
* @property {() => void} release Let the base session go (the next question makes it again).
|
|
296
|
+
*/
|
|
297
|
+
/**
|
|
298
|
+
* A base session over a Prompt API, made when first needed and kept, each
|
|
299
|
+
* question asked of a `clone()` within its time limit.
|
|
300
|
+
*
|
|
301
|
+
* @param {{model: () => LanguageModelStatic | undefined, options: LanguageModelOptions, initialPrompts?: InitialPrompt[]}} setup -
|
|
302
|
+
* `model`: the Prompt API, read when used; `options`: what sessions are made (and availability asked)
|
|
303
|
+
* with; `initialPrompts`: the instructions and examples every question starts from.
|
|
304
|
+
* @returns {ModelSession} The session.
|
|
305
|
+
* @example
|
|
306
|
+
* const sessions = createModelSession({
|
|
307
|
+
* model: chromeLanguageModel,
|
|
308
|
+
* options: { expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }] },
|
|
309
|
+
* initialPrompts: [{ role: 'system', content: 'You name downloaded files.' }],
|
|
310
|
+
* });
|
|
311
|
+
* const { text } = await sessions.ask('Site: acme.com', { timeoutMs: 2500 }); // null when late
|
|
312
|
+
*/
|
|
313
|
+
export declare function createModelSession({ model, options, initialPrompts }: {
|
|
314
|
+
model: () => LanguageModelStatic | undefined;
|
|
315
|
+
options: LanguageModelOptions;
|
|
316
|
+
initialPrompts?: InitialPrompt[];
|
|
317
|
+
}): ModelSession;
|