@arraypress/extension-kit 1.6.0 → 1.8.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 +117 -4
- package/package.json +2 -2
- package/src/ai/model.d.ts +317 -0
- package/src/ai/model.js +312 -0
- package/src/build/store-frame.d.ts +10 -91
- package/src/build/store-frame.js +11 -159
- 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/frame/window.d.ts +120 -0
- package/src/frame/window.js +197 -0
- package/src/index.d.ts +36 -4
- package/src/index.js +25 -4
- package/src/settings/saver.d.ts +13 -0
- package/src/settings/saver.js +11 -0
- package/src/styles/chrome-ui.css +11 -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/heartbeat.d.ts +23 -0
- package/src/testing/heartbeat.js +43 -0
- package/src/testing/index.d.ts +13 -3
- package/src/testing/index.js +10 -3
- package/src/testing/messages.d.ts +33 -0
- package/src/testing/messages.js +51 -0
- package/src/testing/prompt.d.ts +20 -0
- package/src/testing/prompt.js +29 -0
- 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
|
@@ -13,7 +13,14 @@ The pieces ArrayPress's Chrome extensions share, so each is written once:
|
|
|
13
13
|
- **Chrome's page components**: the ⋮ action menu and its row button, the text dialog
|
|
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
|
+
- **Chrome's window around a capture**: the tab strip and toolbar, light or dark, as HTML for store
|
|
17
|
+
screenshots and as an SVG image an extension page draws on a canvas.
|
|
16
18
|
- **Sites**: what people type into a per-site list, and whether a page is on one.
|
|
19
|
+
- **Chrome's on-device model** (Gemini Nano, the Prompt API): availability, a download from the
|
|
20
|
+
person's click with progress, one base session with a `clone()` per question, a time limit on
|
|
21
|
+
every answer, cold starts, and the settings page's “Chrome’s built-in AI” row.
|
|
22
|
+
- **Content-script helpers**: a big page worked through in slices that never hold it long, and a
|
|
23
|
+
change watcher for single-page apps and endless lists.
|
|
17
24
|
- **Build and test helpers** (Node): icon rendering, vendoring npm packages (with a staleness
|
|
18
25
|
check), test builds, store screenshots, zipping for the Chrome Web Store, a fake
|
|
19
26
|
`chrome.storage`, Playwright helpers with time limits, packaging checks, and a copied-code guard.
|
|
@@ -103,6 +110,8 @@ chrome.storage.onChanged.addListener((changes, area) => {
|
|
|
103
110
|
const saver = createSettingsSaver(store, chrome.storage, { onSaved: showSaved });
|
|
104
111
|
saver.save({ menuStyle: 'full' });
|
|
105
112
|
addEventListener('pagehide', () => saver.flush());
|
|
113
|
+
// Changed in another tab or on another computer: shown, with this page's unwritten changes over it.
|
|
114
|
+
saver.follow((settings) => show(settings), chrome.storage.onChanged);
|
|
106
115
|
```
|
|
107
116
|
|
|
108
117
|
| Store member | What it does |
|
|
@@ -178,6 +187,25 @@ takes it as a placeholder whose content is `$1`, and every lookup without substi
|
|
|
178
187
|
<h2 class="section-title" data-i18n="optionsAbout"></h2> <!-- About Upright -->
|
|
179
188
|
```
|
|
180
189
|
|
|
190
|
+
## Chrome's window around a capture
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
import { WINDOW_BARS, letterIcon, windowChromeSvg } from '../vendor/extension-kit/index.js';
|
|
194
|
+
|
|
195
|
+
const svg = windowChromeSvg({ tabs: [{ title: 'Example', icon: letterIcon('E', '#1a73e8'), active: true }], url: 'example.com/a', theme: 'dark', w: 1280 });
|
|
196
|
+
const bars = new Image();
|
|
197
|
+
bars.src = `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
198
|
+
await bars.decode();
|
|
199
|
+
context.drawImage(bars, 0, 0, 1280 * scale, (WINDOW_BARS.strip + WINDOW_BARS.toolbar) * scale); // the capture goes below
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The same macOS window the store screenshots use (`storeFrame.windowChrome`, now `frame/window.js`
|
|
203
|
+
in the runtime half): `windowChrome(options)` is its HTML, `windowChromeSvg(options)` that HTML in
|
|
204
|
+
an SVG image `w` wide and `WINDOW_BARS.strip + WINDOW_BARS.toolbar` (88px) high, drawn sharp at any
|
|
205
|
+
scale. Chrome draws an SVG image with HTML inside (`foreignObject`) without tainting the canvas
|
|
206
|
+
(checked in Chromium, `tests/frame-chromium.test.js`). Images in it must be `data:` URLs
|
|
207
|
+
(`letterIcon` makes one). `THEMES` holds the light and dark colours.
|
|
208
|
+
|
|
181
209
|
## Sites
|
|
182
210
|
|
|
183
211
|
```js
|
|
@@ -211,6 +239,81 @@ Registers, updates or unregisters the script `id` to match what `want()` gives a
|
|
|
211
239
|
none). Calls run one after another, so a quick run of changes can't register twice or end out of
|
|
212
240
|
order; a refused change rejects that call only.
|
|
213
241
|
|
|
242
|
+
## Chrome's on-device model
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
import { chromeLanguageModel, createModelSession } from '../vendor/extension-kit/index.js';
|
|
246
|
+
|
|
247
|
+
const ENGLISH = { expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }] };
|
|
248
|
+
const model = createModelSession({
|
|
249
|
+
model: chromeLanguageModel, // read when used: Chrome may add it after start-up
|
|
250
|
+
options: ENGLISH, // availability() is asked with what create() gets
|
|
251
|
+
initialPrompts: [{ role: 'system', content: 'You name downloaded files.' }, ...EXAMPLE],
|
|
252
|
+
});
|
|
253
|
+
const answer = await model.ask(prompt, { timeoutMs: 2500, signal, responseConstraint: schema });
|
|
254
|
+
if (answer.text === null) useRules(answer.reason); // 'unavailable' | 'late' | 'aborted' | 'failed'
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Following https://developer.chrome.com/docs/ai/prompt-api: only an `available` model is used (a
|
|
258
|
+
question never downloads it); one base session is made when first needed and kept; each question
|
|
259
|
+
goes to a `clone()` of it, let go after; the answer's `AbortSignal` fires at the time limit. The
|
|
260
|
+
first session after Chrome starts can take ~18 s while the model loads: a question with a shorter
|
|
261
|
+
limit comes back `'late'`, and the session goes on loading for the next one. Nothing loads it ahead
|
|
262
|
+
of time (that would load the model on every start). `release()` lets the base session go.
|
|
263
|
+
|
|
264
|
+
`modelAvailability(api, options)` never throws (`'missing'` without a Prompt API).
|
|
265
|
+
`downloadModel(api, options, {onProgress})` is the only `create()` that may download: call it
|
|
266
|
+
first in a click's handler, with nothing awaited before it. The settings row:
|
|
267
|
+
|
|
268
|
+
```html
|
|
269
|
+
<div class="row two-line">
|
|
270
|
+
<span class="label">
|
|
271
|
+
<span data-i18n="optionsModel">Chrome’s built-in AI</span>
|
|
272
|
+
<span class="secondary"><span id="model-status"></span> <span id="model-help" hidden></span></span>
|
|
273
|
+
<progress id="model-progress" max="1" value="0" hidden></progress>
|
|
274
|
+
</span>
|
|
275
|
+
<button id="model-download" type="button" data-i18n="optionsModelDownload" hidden>Download</button>
|
|
276
|
+
</div>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
const row = createModelRow(
|
|
281
|
+
{ status: byId('model-status'), help: byId('model-help'), download: byId('model-download', HTMLButtonElement), progress: byId('model-progress', HTMLProgressElement) },
|
|
282
|
+
{ options: ENGLISH, state: () => ask(MESSAGES.modelState), t, onShown: ({ state }) => { useAi.disabled = state === 'off'; } },
|
|
283
|
+
);
|
|
284
|
+
await row.refresh();
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Its words are the page's messages under `MODEL_MESSAGES`' keys (`optionsModel`, `optionsModelReady`,
|
|
288
|
+
`optionsModelNeedsDownload`, `optionsModelDownloadHelp`, `optionsModelDownload`,
|
|
289
|
+
`optionsModelDownloading` with `$PERCENT$`, `optionsModelDownloadFailed`, `optionsModelUnsupported`,
|
|
290
|
+
`optionsModelRequirements`, `optionsModelMissing`, `optionsModelMissingHelp`); `describeModel` maps
|
|
291
|
+
a state to them. Tests use `fakeLanguageModel` (`/testing`), the Prompt API's shape: availability,
|
|
292
|
+
download progress (or a failed download), a cold start, slow and failed answers, aborts, and a log
|
|
293
|
+
of what it was asked.
|
|
294
|
+
|
|
295
|
+
## Content scripts
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
import { inSlices } from '../vendor/extension-kit/content/slices.js';
|
|
299
|
+
import { watchChanges } from '../vendor/extension-kit/content/watch.js';
|
|
300
|
+
|
|
301
|
+
await inSlices(document.body, (block) => scanner.scan(block)); // ≤4,000 characters a block, 40 ms slices
|
|
302
|
+
const stop = watchChanges(document.body, {
|
|
303
|
+
changed: (nodes) => nodes.forEach((node) => scanner.scan(node)), // one batch, each subtree once
|
|
304
|
+
removed: (node) => scanner.forget(node), // at once
|
|
305
|
+
ignore: (node) => node instanceof Element && node.localName === 'my-note', // the extension's own
|
|
306
|
+
settleMs: 120, maxWaitMs: 600,
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`inSlices(root, work, {blockChars, sliceMs, pause})` cuts a page into blocks (`blocksOf`) and
|
|
311
|
+
hands the main thread back whenever a slice has run its time, so a 2,000-item listing never
|
|
312
|
+
freezes. `watchChanges` hands over what was added or changed a little after the page stops
|
|
313
|
+
changing (at most `maxWaitMs` while it keeps on); an element with a few children (`regroupChildren`,
|
|
314
|
+
default 6) whose children changed is handed over whole. Both are pure DOM, so a content script
|
|
315
|
+
imports them directly (list them in `web_accessible_resources`).
|
|
316
|
+
|
|
214
317
|
## Time limits
|
|
215
318
|
|
|
216
319
|
```js
|
|
@@ -273,7 +376,11 @@ const site = await askText({
|
|
|
273
376
|
});
|
|
274
377
|
```
|
|
275
378
|
|
|
276
|
-
Resolves to the text, trimmed, or `null` when cancelled.
|
|
379
|
+
Resolves to the text, trimmed, or `null` when cancelled. A field can be `multiline` (a textarea,
|
|
380
|
+
`rows` high: Enter starts a new line), carry a `hint` under it (or a function of its text, kept up
|
|
381
|
+
as it's typed: “12 words”), and take a `value` that's still being worked out (a promise, say an
|
|
382
|
+
on-device model's suggestion): the field waits, disabled, with `waiting` under it, then takes the
|
|
383
|
+
text and the focus (unless the person has typed meanwhile); a failed one leaves it empty. The action button (default "Save") stays
|
|
277
384
|
disabled while the text is blank or `validate` gives a reason, which shows under the field
|
|
278
385
|
(`checkText` is that rule, pure). Enter presses the action button; Esc and Cancel cancel; the
|
|
279
386
|
focus goes back to what had it, or to `returnTo` (an element, or a function asked at the end).
|
|
@@ -372,7 +479,7 @@ A list with an Add button needs every key but `optionsRemoveLabel`; one with an
|
|
|
372
479
|
| `renderIcons`, `renderToolbarIcons({chromium, dir})` | See above. |
|
|
373
480
|
| `buildTestCopy({from, to, hook, manifest})` | A test build: the extension, `hook` as its service worker (`e2e-hook.js`), the manifest changed. |
|
|
374
481
|
| `testBuildDir(root, name)` | `.test-builds/<name>`, or under `$EXTENSION_TEST_BUILDS` (to test another build, or run two side by side). |
|
|
375
|
-
| `storeFrame` | Store screenshots: `canvas`, `windowChrome`, `geometry`, `letterIcon`, `cursor`, `THEMES`, `W`×`H`. |
|
|
482
|
+
| `storeFrame` | Store screenshots: `canvas`, `windowChrome` (and `windowChromeSvg`, from `frame/window.js`), `geometry`, `letterIcon`, `cursor`, `THEMES`, `W`×`H`. |
|
|
376
483
|
| `placeholderWarning`, `repositoryReminder`, `printReleaseChecks` | See About links. |
|
|
377
484
|
| `findDisallowedClones({runJscpd})` | Run jscpd and return clones `.jscpd-allowlist.json` doesn't cover (the `extension-kit duplication` command). |
|
|
378
485
|
|
|
@@ -389,10 +496,15 @@ except those listed, each with a reason:
|
|
|
389
496
|
|
|
390
497
|
| Export | What it does |
|
|
391
498
|
|---|---|
|
|
392
|
-
| `
|
|
499
|
+
| `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`. |
|
|
500
|
+
| `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. |
|
|
393
501
|
| `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
|
|
394
502
|
| `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. |
|
|
395
503
|
| `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()}`. |
|
|
504
|
+
| `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. |
|
|
505
|
+
| `messageLookup(extensionDir, locale)`, `readMessages(extensionDir, locale)` | `chrome.i18n.getMessage` over the real `_locales` (a locale over en, named placeholders filled), for unit tests. |
|
|
506
|
+
| `heartbeat(page)` | A timer beating in a page: `stop()` gives the longest the main thread was held (ms), for performance checks. |
|
|
507
|
+
| `playPermissionPrompt(answers)` | Chrome's permission prompt played in a page (`page.evaluate` or `addInitScript`): each request recorded on `window.asked` with whether a click was behind it, answered from `answers`. |
|
|
396
508
|
| `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. |
|
|
397
509
|
| `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)` (also in the main entry). |
|
|
398
510
|
| `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
|
|
@@ -435,6 +547,7 @@ except those listed, each with a reason:
|
|
|
435
547
|
| Manager layout | `.manager`/`.layout`, `.sidebar` with `.tree` > `.tree-item`, `.main` with `.list-card` |
|
|
436
548
|
| Menu (cr-action-menu) | `<dialog class="menu">` of `button[role="menuitem"]`, `hr` (or `createMenu`) |
|
|
437
549
|
| ⋮ icon | `<span class="icon icon-more-vert" aria-hidden="true">` (or `moreActionsButton`) |
|
|
550
|
+
| Subpage header (settings-subpage) | `<div class="subpage-head">` with a back `button.icon-button` holding `.icon.icon-arrow-back`, then `<h2>` |
|
|
438
551
|
| Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (`askText`, `askFields`) |
|
|
439
552
|
| 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 |
|
|
440
553
|
| Drawer (cr-drawer) | `<dialog class="drawer">` with `.drawer-head` and a `.nav` |
|
|
@@ -455,7 +568,7 @@ chrome://bookmarks in Chromium 156 / Chrome 157; the sources are listed at the t
|
|
|
455
568
|
## Development
|
|
456
569
|
|
|
457
570
|
`npm test` runs ESLint, a strict TypeScript check of the JSDoc (`checkJs`), the tests (the menu,
|
|
458
|
-
the dialog, the toast, the dialog site list, icon rendering and the Chrome readers in Playwright's
|
|
571
|
+
the dialog, the toast, the dialog site list, the window frame, icon rendering and the Chrome readers in Playwright's
|
|
459
572
|
Chromium, the rest in Node and jsdom), and the
|
|
460
573
|
copied-code guard. The `.d.ts` files are generated from the JSDoc: `npm run types` writes them, and
|
|
461
574
|
the type check fails while one is stale (it regenerates them into `.types/` and compares).
|
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.8.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;
|