@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/src/testing/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The kit's test helpers: a fake `chrome.storage`, Playwright helpers for
|
|
2
|
+
* The kit's test helpers: a fake `chrome.storage`, a fake on-device model, Playwright helpers for
|
|
3
3
|
* end-to-end tests (time limits, a pool, a browser that passes for a
|
|
4
4
|
* person's) and a server for their pages, Chrome's install warnings and
|
|
5
5
|
* saved tab groups, and checks for a packaging test. Node only.
|
|
@@ -8,19 +8,26 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
|
|
10
10
|
export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
|
|
11
|
-
export { CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until, } from './e2e.js';
|
|
11
|
+
export { CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, runChecks, until, } from './e2e.js';
|
|
12
12
|
export { exitPoint, parseLiveOptions, parseProxy } from './live.js';
|
|
13
13
|
export { eachLimited } from '../time/pool.js';
|
|
14
14
|
export { filesIn, launchDownloading } from './downloads.js';
|
|
15
|
+
export { fakeLanguageModel } from './fake-model.js';
|
|
16
|
+
export { messageLookup, readMessages } from './messages.js';
|
|
17
|
+
export { heartbeat } from './heartbeat.js';
|
|
18
|
+
export { playPermissionPrompt } from './prompt.js';
|
|
15
19
|
export { serve, testCertificate } from './server.js';
|
|
16
|
-
export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
|
|
20
|
+
export { SYNC_QUOTA_BYTES, SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
|
|
17
21
|
export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
|
|
18
22
|
export { savedTabGroups } from './saved-groups.js';
|
|
19
23
|
export { installWarnings, warningsAddedBy, withoutPermissions } from './warnings.js';
|
|
20
24
|
export type CheckRunner = import('./e2e.js').CheckRunner;
|
|
25
|
+
export type FakeLanguageModel = import('./fake-model.js').FakeLanguageModel;
|
|
26
|
+
export type FakeModelState = import('./fake-model.js').FakeModelState;
|
|
21
27
|
export type ExtensionProfile = import('./e2e.js').ExtensionProfile;
|
|
22
28
|
export type LaunchedExtension = import('./e2e.js').LaunchedExtension;
|
|
23
29
|
export type LaunchOptions = import('./e2e.js').LaunchOptions;
|
|
30
|
+
export type Messages = import('./messages.js').Messages;
|
|
24
31
|
export type Exit = import('./live.js').Exit;
|
|
25
32
|
export type LiveOptions = import('./live.js').LiveOptions;
|
|
26
33
|
export type SavedTabGroup = import('./saved-groups.js').SavedTabGroup;
|
|
@@ -32,9 +39,12 @@ export type TestServer = import('./server.js').TestServer;
|
|
|
32
39
|
* The helpers' types, for a test's JSDoc.
|
|
33
40
|
*
|
|
34
41
|
* @typedef {import('./e2e.js').CheckRunner} CheckRunner
|
|
42
|
+
* @typedef {import('./fake-model.js').FakeLanguageModel} FakeLanguageModel
|
|
43
|
+
* @typedef {import('./fake-model.js').FakeModelState} FakeModelState
|
|
35
44
|
* @typedef {import('./e2e.js').ExtensionProfile} ExtensionProfile
|
|
36
45
|
* @typedef {import('./e2e.js').LaunchedExtension} LaunchedExtension
|
|
37
46
|
* @typedef {import('./e2e.js').LaunchOptions} LaunchOptions
|
|
47
|
+
* @typedef {import('./messages.js').Messages} Messages
|
|
38
48
|
* @typedef {import('./live.js').Exit} Exit
|
|
39
49
|
* @typedef {import('./live.js').LiveOptions} LiveOptions
|
|
40
50
|
* @typedef {import('./saved-groups.js').SavedTabGroup} SavedTabGroup
|
package/src/testing/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The kit's test helpers: a fake `chrome.storage`, Playwright helpers for
|
|
2
|
+
* The kit's test helpers: a fake `chrome.storage`, a fake on-device model, Playwright helpers for
|
|
3
3
|
* end-to-end tests (time limits, a pool, a browser that passes for a
|
|
4
4
|
* person's) and a server for their pages, Chrome's install warnings and
|
|
5
5
|
* saved tab groups, and checks for a packaging test. Node only.
|
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
|
|
11
11
|
export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
|
|
12
12
|
export {
|
|
13
|
-
CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until,
|
|
13
|
+
CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, runChecks, until,
|
|
14
14
|
} from './e2e.js';
|
|
15
15
|
export { exitPoint, parseLiveOptions, parseProxy } from './live.js';
|
|
16
16
|
export { eachLimited } from '../time/pool.js';
|
|
17
17
|
export { filesIn, launchDownloading } from './downloads.js';
|
|
18
|
+
export { fakeLanguageModel } from './fake-model.js';
|
|
19
|
+
export { messageLookup, readMessages } from './messages.js';
|
|
20
|
+
export { heartbeat } from './heartbeat.js';
|
|
21
|
+
export { playPermissionPrompt } from './prompt.js';
|
|
18
22
|
export { serve, testCertificate } from './server.js';
|
|
19
|
-
export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
|
|
23
|
+
export { SYNC_QUOTA_BYTES, SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
|
|
20
24
|
export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
|
|
21
25
|
export { savedTabGroups } from './saved-groups.js';
|
|
22
26
|
export { installWarnings, warningsAddedBy, withoutPermissions } from './warnings.js';
|
|
@@ -25,9 +29,12 @@ export { installWarnings, warningsAddedBy, withoutPermissions } from './warnings
|
|
|
25
29
|
* The helpers' types, for a test's JSDoc.
|
|
26
30
|
*
|
|
27
31
|
* @typedef {import('./e2e.js').CheckRunner} CheckRunner
|
|
32
|
+
* @typedef {import('./fake-model.js').FakeLanguageModel} FakeLanguageModel
|
|
33
|
+
* @typedef {import('./fake-model.js').FakeModelState} FakeModelState
|
|
28
34
|
* @typedef {import('./e2e.js').ExtensionProfile} ExtensionProfile
|
|
29
35
|
* @typedef {import('./e2e.js').LaunchedExtension} LaunchedExtension
|
|
30
36
|
* @typedef {import('./e2e.js').LaunchOptions} LaunchOptions
|
|
37
|
+
* @typedef {import('./messages.js').Messages} Messages
|
|
31
38
|
* @typedef {import('./live.js').Exit} Exit
|
|
32
39
|
* @typedef {import('./live.js').LiveOptions} LiveOptions
|
|
33
40
|
* @typedef {import('./saved-groups.js').SavedTabGroup} SavedTabGroup
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type Messages = Record<string, {
|
|
2
|
+
message: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
placeholders?: Record<string, {
|
|
5
|
+
content: string;
|
|
6
|
+
}>;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* One locale's messages.json.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {Record<string, {message: string, description?: string, placeholders?: Record<string, {content: string}>}>} Messages
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Read a locale's messages.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} extensionDir - The extension's folder (with `_locales/`).
|
|
17
|
+
* @param {string} locale - "en" or "en_GB".
|
|
18
|
+
* @returns {Messages} Its messages.
|
|
19
|
+
* @example
|
|
20
|
+
* readMessages('extension', 'en_GB').optionsColour.message;
|
|
21
|
+
*/
|
|
22
|
+
export declare function readMessages(extensionDir: string, locale: string): Messages;
|
|
23
|
+
/**
|
|
24
|
+
* `chrome.i18n.getMessage` over a locale's real messages ('' for a missing key, as Chrome gives).
|
|
25
|
+
*
|
|
26
|
+
* @param {string} extensionDir - The extension's folder.
|
|
27
|
+
* @param {string} [locale] - Default "en"; another locale is read over en.
|
|
28
|
+
* @returns {(key: string, substitutions?: string | string[]) => string} The lookup.
|
|
29
|
+
* @example
|
|
30
|
+
* const t = messageLookup('extension');
|
|
31
|
+
* t('optionsMoreActions', ['example.com']); // 'More actions for example.com'
|
|
32
|
+
*/
|
|
33
|
+
export declare function messageLookup(extensionDir: string, locale?: string): (key: string, substitutions?: string | string[]) => string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome's message lookup for an extension's unit tests: its real `_locales`
|
|
3
|
+
* files, a locale's messages over `en`'s, named placeholders ("$SITE$" →
|
|
4
|
+
* `{site: {content: '$1'}}`) filled from substitutions as
|
|
5
|
+
* `chrome.i18n.getMessage` fills them. Node only. From the copies in Better
|
|
6
|
+
* Prices, Better Downloads, Better Saves and Clean Links (`test/helpers/i18n.js`).
|
|
7
|
+
*
|
|
8
|
+
* @module testing/messages
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One locale's messages.json.
|
|
15
|
+
*
|
|
16
|
+
* @typedef {Record<string, {message: string, description?: string, placeholders?: Record<string, {content: string}>}>} Messages
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read a locale's messages.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} extensionDir - The extension's folder (with `_locales/`).
|
|
23
|
+
* @param {string} locale - "en" or "en_GB".
|
|
24
|
+
* @returns {Messages} Its messages.
|
|
25
|
+
* @example
|
|
26
|
+
* readMessages('extension', 'en_GB').optionsColour.message;
|
|
27
|
+
*/
|
|
28
|
+
export function readMessages(extensionDir, locale) {
|
|
29
|
+
return JSON.parse(readFileSync(path.join(extensionDir, '_locales', locale, 'messages.json'), 'utf8'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `chrome.i18n.getMessage` over a locale's real messages ('' for a missing key, as Chrome gives).
|
|
34
|
+
*
|
|
35
|
+
* @param {string} extensionDir - The extension's folder.
|
|
36
|
+
* @param {string} [locale] - Default "en"; another locale is read over en.
|
|
37
|
+
* @returns {(key: string, substitutions?: string | string[]) => string} The lookup.
|
|
38
|
+
* @example
|
|
39
|
+
* const t = messageLookup('extension');
|
|
40
|
+
* t('optionsMoreActions', ['example.com']); // 'More actions for example.com'
|
|
41
|
+
*/
|
|
42
|
+
export function messageLookup(extensionDir, locale = 'en') {
|
|
43
|
+
const messages = { ...readMessages(extensionDir, 'en'), ...(locale === 'en' ? {} : readMessages(extensionDir, locale)) };
|
|
44
|
+
return (key, substitutions = []) => {
|
|
45
|
+
const entry = messages[key];
|
|
46
|
+
if (!entry) return '';
|
|
47
|
+
const subs = [substitutions].flat();
|
|
48
|
+
return entry.message.replace(/\$([A-Za-z_]+)\$/g, (_, name) => (entry.placeholders?.[name.toLowerCase()]?.content ?? '')
|
|
49
|
+
.replace(/\$(\d)/g, (/** @type {string} */ __, /** @type {string} */ n) => subs[Number(n) - 1] ?? ''));
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome's permission prompt, played in an extension page for an e2e test: Playwright can't
|
|
3
|
+
* click Chrome's own prompt. From Topicless' and Better Screenshots' e2e helpers.
|
|
4
|
+
*
|
|
5
|
+
* @module testing/prompt
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Stand in for Chrome's permission prompt in a page: each `chrome.permissions.request` is
|
|
9
|
+
* recorded on `window.asked` (what was asked, and whether a click was behind it,
|
|
10
|
+
* `navigator.userActivation.isActive`) and answered from `answers` (the next one; the last is
|
|
11
|
+
* kept), as a person clicking “Allow” or “Deny” would. Runs in the page: give it to
|
|
12
|
+
* `page.evaluate` or `page.addInitScript`, so it takes no closures.
|
|
13
|
+
*
|
|
14
|
+
* @param {boolean[]} answers - The answers, in order.
|
|
15
|
+
* @example
|
|
16
|
+
* await page.evaluate(playPermissionPrompt, [true]);
|
|
17
|
+
* await page.click('#download');
|
|
18
|
+
* const asked = await page.evaluate(() => window.asked); // [{ permissions: ['downloads'], origins: undefined, gesture: true }]
|
|
19
|
+
*/
|
|
20
|
+
export declare function playPermissionPrompt(answers: boolean[]): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome's permission prompt, played in an extension page for an e2e test: Playwright can't
|
|
3
|
+
* click Chrome's own prompt. From Topicless' and Better Screenshots' e2e helpers.
|
|
4
|
+
*
|
|
5
|
+
* @module testing/prompt
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Stand in for Chrome's permission prompt in a page: each `chrome.permissions.request` is
|
|
10
|
+
* recorded on `window.asked` (what was asked, and whether a click was behind it,
|
|
11
|
+
* `navigator.userActivation.isActive`) and answered from `answers` (the next one; the last is
|
|
12
|
+
* kept), as a person clicking “Allow” or “Deny” would. Runs in the page: give it to
|
|
13
|
+
* `page.evaluate` or `page.addInitScript`, so it takes no closures.
|
|
14
|
+
*
|
|
15
|
+
* @param {boolean[]} answers - The answers, in order.
|
|
16
|
+
* @example
|
|
17
|
+
* await page.evaluate(playPermissionPrompt, [true]);
|
|
18
|
+
* await page.click('#download');
|
|
19
|
+
* const asked = await page.evaluate(() => window.asked); // [{ permissions: ['downloads'], origins: undefined, gesture: true }]
|
|
20
|
+
*/
|
|
21
|
+
export function playPermissionPrompt(answers) {
|
|
22
|
+
/** @type {{permissions?: string[], origins?: string[], gesture: boolean}[]} */
|
|
23
|
+
const asked = [];
|
|
24
|
+
Object.assign(window, { asked });
|
|
25
|
+
chrome.permissions.request = /** @type {typeof chrome.permissions.request} */ (/** @type {unknown} */ (async (/** @type {chrome.permissions.Permissions} */ permissions) => {
|
|
26
|
+
asked.push({ permissions: permissions.permissions, origins: permissions.origins, gesture: navigator.userActivation.isActive });
|
|
27
|
+
return answers.length > 1 ? answers.shift() : answers[0];
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings row “Chrome’s built-in AI”: how the on-device model stands,
|
|
3
|
+
* what that means, and Download when it can be had, with Chrome's progress.
|
|
4
|
+
* The download starts from the click (Chrome only downloads from a user
|
|
5
|
+
* gesture): nothing is awaited in the click before `create()`. The words are
|
|
6
|
+
* the page's messages under {@link MODEL_MESSAGES}' keys.
|
|
7
|
+
*
|
|
8
|
+
* Markup it fills (the page's, so it sits in any card):
|
|
9
|
+
*
|
|
10
|
+
* ```html
|
|
11
|
+
* <div class="row two-line">
|
|
12
|
+
* <span class="label">
|
|
13
|
+
* <span data-i18n="optionsModel">Chrome’s built-in AI</span>
|
|
14
|
+
* <span class="secondary"><span id="model-status"></span> <span id="model-help" hidden></span></span>
|
|
15
|
+
* <progress id="model-progress" max="1" value="0" hidden></progress>
|
|
16
|
+
* </span>
|
|
17
|
+
* <button id="model-download" type="button" data-i18n="optionsModelDownload" hidden>Download</button>
|
|
18
|
+
* </div>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @module ui/model-row
|
|
22
|
+
*/
|
|
23
|
+
import type { Availability, LanguageModelOptions, LanguageModelStatic, ModelStatus } from '../ai/model.js';
|
|
24
|
+
export type ModelRowParts = {
|
|
25
|
+
/**
|
|
26
|
+
* The state's words.
|
|
27
|
+
*/
|
|
28
|
+
status: HTMLElement;
|
|
29
|
+
/**
|
|
30
|
+
* What it means, or why a download failed.
|
|
31
|
+
*/
|
|
32
|
+
help: HTMLElement;
|
|
33
|
+
/**
|
|
34
|
+
* Download.
|
|
35
|
+
*/
|
|
36
|
+
download: HTMLButtonElement;
|
|
37
|
+
/**
|
|
38
|
+
* The download's progress, 0–1.
|
|
39
|
+
*/
|
|
40
|
+
progress: HTMLProgressElement;
|
|
41
|
+
};
|
|
42
|
+
export type ModelRowOptions = {
|
|
43
|
+
/**
|
|
44
|
+
* What sessions are made with (a download is for these).
|
|
45
|
+
*/
|
|
46
|
+
options: LanguageModelOptions;
|
|
47
|
+
/**
|
|
48
|
+
* The model's state as the code that uses it sees it (the
|
|
49
|
+
* worker's, asked by message, when the worker uses it).
|
|
50
|
+
*/
|
|
51
|
+
state: () => Promise<Availability>;
|
|
52
|
+
/**
|
|
53
|
+
* The page's messages.
|
|
54
|
+
*/
|
|
55
|
+
t: (key: string, substitutions?: string | string[]) => string;
|
|
56
|
+
/**
|
|
57
|
+
* The Prompt API in this page. Default Chrome's.
|
|
58
|
+
*/
|
|
59
|
+
model?: () => LanguageModelStatic | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Hears each state shown
|
|
62
|
+
* (to turn the switches that need the model on or off).
|
|
63
|
+
*/
|
|
64
|
+
onShown?: (status: ModelStatus, availability: Availability) => void;
|
|
65
|
+
};
|
|
66
|
+
export type ModelRow = {
|
|
67
|
+
/**
|
|
68
|
+
* Ask the state again and show it.
|
|
69
|
+
*/
|
|
70
|
+
refresh: () => Promise<Availability>;
|
|
71
|
+
/**
|
|
72
|
+
* The state last shown (null before the first).
|
|
73
|
+
*/
|
|
74
|
+
current: () => Availability | null;
|
|
75
|
+
};
|
|
76
|
+
/** @import { Availability, LanguageModelOptions, LanguageModelStatic, ModelStatus } from '../ai/model.js' */
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {object} ModelRowParts
|
|
79
|
+
* @property {HTMLElement} status The state's words.
|
|
80
|
+
* @property {HTMLElement} help What it means, or why a download failed.
|
|
81
|
+
* @property {HTMLButtonElement} download Download.
|
|
82
|
+
* @property {HTMLProgressElement} progress The download's progress, 0–1.
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* @typedef {object} ModelRowOptions
|
|
86
|
+
* @property {LanguageModelOptions} options What sessions are made with (a download is for these).
|
|
87
|
+
* @property {() => Promise<Availability>} state The model's state as the code that uses it sees it (the
|
|
88
|
+
* worker's, asked by message, when the worker uses it).
|
|
89
|
+
* @property {(key: string, substitutions?: string | string[]) => string} t The page's messages.
|
|
90
|
+
* @property {() => LanguageModelStatic | undefined} [model] The Prompt API in this page. Default Chrome's.
|
|
91
|
+
* @property {(status: ModelStatus, availability: Availability) => void} [onShown] Hears each state shown
|
|
92
|
+
* (to turn the switches that need the model on or off).
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* @typedef {object} ModelRow
|
|
96
|
+
* @property {() => Promise<Availability>} refresh Ask the state again and show it.
|
|
97
|
+
* @property {() => Availability | null} current The state last shown (null before the first).
|
|
98
|
+
*/
|
|
99
|
+
/**
|
|
100
|
+
* Wire the row up and show the model's state.
|
|
101
|
+
*
|
|
102
|
+
* @param {ModelRowParts} parts - The row's elements.
|
|
103
|
+
* @param {ModelRowOptions} setup - What to ask, and the words.
|
|
104
|
+
* @returns {ModelRow} The row.
|
|
105
|
+
* @example
|
|
106
|
+
* const row = createModelRow({ status: byId('model-status'), help: byId('model-help'),
|
|
107
|
+
* download: byId('model-download', HTMLButtonElement), progress: byId('model-progress', HTMLProgressElement) },
|
|
108
|
+
* { options: SESSION_OPTIONS, state: () => ask(MESSAGES.modelState), t, onShown: ({ state }) => { useAi.disabled = state === 'off'; } });
|
|
109
|
+
* await row.refresh();
|
|
110
|
+
*/
|
|
111
|
+
export declare function createModelRow({ status, help, download, progress }: ModelRowParts, { options, state, t, model, onShown }: ModelRowOptions): ModelRow;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings row “Chrome’s built-in AI”: how the on-device model stands,
|
|
3
|
+
* what that means, and Download when it can be had, with Chrome's progress.
|
|
4
|
+
* The download starts from the click (Chrome only downloads from a user
|
|
5
|
+
* gesture): nothing is awaited in the click before `create()`. The words are
|
|
6
|
+
* the page's messages under {@link MODEL_MESSAGES}' keys.
|
|
7
|
+
*
|
|
8
|
+
* Markup it fills (the page's, so it sits in any card):
|
|
9
|
+
*
|
|
10
|
+
* ```html
|
|
11
|
+
* <div class="row two-line">
|
|
12
|
+
* <span class="label">
|
|
13
|
+
* <span data-i18n="optionsModel">Chrome’s built-in AI</span>
|
|
14
|
+
* <span class="secondary"><span id="model-status"></span> <span id="model-help" hidden></span></span>
|
|
15
|
+
* <progress id="model-progress" max="1" value="0" hidden></progress>
|
|
16
|
+
* </span>
|
|
17
|
+
* <button id="model-download" type="button" data-i18n="optionsModelDownload" hidden>Download</button>
|
|
18
|
+
* </div>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @module ui/model-row
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { MODEL_MESSAGES, chromeLanguageModel, describeModel, downloadModel } from '../ai/model.js';
|
|
25
|
+
|
|
26
|
+
/** @import { Availability, LanguageModelOptions, LanguageModelStatic, ModelStatus } from '../ai/model.js' */
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} ModelRowParts
|
|
30
|
+
* @property {HTMLElement} status The state's words.
|
|
31
|
+
* @property {HTMLElement} help What it means, or why a download failed.
|
|
32
|
+
* @property {HTMLButtonElement} download Download.
|
|
33
|
+
* @property {HTMLProgressElement} progress The download's progress, 0–1.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} ModelRowOptions
|
|
38
|
+
* @property {LanguageModelOptions} options What sessions are made with (a download is for these).
|
|
39
|
+
* @property {() => Promise<Availability>} state The model's state as the code that uses it sees it (the
|
|
40
|
+
* worker's, asked by message, when the worker uses it).
|
|
41
|
+
* @property {(key: string, substitutions?: string | string[]) => string} t The page's messages.
|
|
42
|
+
* @property {() => LanguageModelStatic | undefined} [model] The Prompt API in this page. Default Chrome's.
|
|
43
|
+
* @property {(status: ModelStatus, availability: Availability) => void} [onShown] Hears each state shown
|
|
44
|
+
* (to turn the switches that need the model on or off).
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {object} ModelRow
|
|
49
|
+
* @property {() => Promise<Availability>} refresh Ask the state again and show it.
|
|
50
|
+
* @property {() => Availability | null} current The state last shown (null before the first).
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wire the row up and show the model's state.
|
|
55
|
+
*
|
|
56
|
+
* @param {ModelRowParts} parts - The row's elements.
|
|
57
|
+
* @param {ModelRowOptions} setup - What to ask, and the words.
|
|
58
|
+
* @returns {ModelRow} The row.
|
|
59
|
+
* @example
|
|
60
|
+
* const row = createModelRow({ status: byId('model-status'), help: byId('model-help'),
|
|
61
|
+
* download: byId('model-download', HTMLButtonElement), progress: byId('model-progress', HTMLProgressElement) },
|
|
62
|
+
* { options: SESSION_OPTIONS, state: () => ask(MESSAGES.modelState), t, onShown: ({ state }) => { useAi.disabled = state === 'off'; } });
|
|
63
|
+
* await row.refresh();
|
|
64
|
+
*/
|
|
65
|
+
export function createModelRow({ status, help, download, progress }, { options, state, t, model = chromeLanguageModel, onShown }) {
|
|
66
|
+
/** @type {Availability | null} */
|
|
67
|
+
let shown = null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Show a state, or why the last download failed.
|
|
71
|
+
*
|
|
72
|
+
* @param {Availability} availability - The model's.
|
|
73
|
+
* @param {string} [problem] - What went wrong.
|
|
74
|
+
*/
|
|
75
|
+
function show(availability, problem = '') {
|
|
76
|
+
const described = describeModel(availability);
|
|
77
|
+
shown = availability;
|
|
78
|
+
status.dataset.state = described.state;
|
|
79
|
+
status.textContent = t(described.status);
|
|
80
|
+
help.textContent = problem || (described.help ? t(described.help) : '');
|
|
81
|
+
help.hidden = !help.textContent;
|
|
82
|
+
download.hidden = !described.canDownload;
|
|
83
|
+
download.disabled = false;
|
|
84
|
+
progress.hidden = true;
|
|
85
|
+
onShown?.(described, availability);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
download.addEventListener('click', () => {
|
|
89
|
+
download.disabled = true;
|
|
90
|
+
progress.hidden = false;
|
|
91
|
+
progress.value = 0;
|
|
92
|
+
downloadModel(model(), options, {
|
|
93
|
+
onProgress: (loaded) => {
|
|
94
|
+
progress.value = loaded;
|
|
95
|
+
status.textContent = t(MODEL_MESSAGES.downloading, [String(Math.round(loaded * 100))]);
|
|
96
|
+
},
|
|
97
|
+
}).then(async () => show(await state()), () => show('downloadable', t(MODEL_MESSAGES.downloadFailed)));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
async refresh() {
|
|
102
|
+
const availability = await state();
|
|
103
|
+
show(availability);
|
|
104
|
+
return availability;
|
|
105
|
+
},
|
|
106
|
+
current: () => shown,
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/ui/text-dialog.d.ts
CHANGED
|
@@ -20,13 +20,33 @@ export type FieldRequest = {
|
|
|
20
20
|
*/
|
|
21
21
|
label: string;
|
|
22
22
|
/**
|
|
23
|
-
* The text to start with. Default ''.
|
|
23
|
+
* The text to start with. Default ''. A promise is text
|
|
24
|
+
* still being worked out (an on-device model's suggestion): the field waits, disabled, with
|
|
25
|
+
* `waiting` under it, and takes the text when it comes ('' if it fails).
|
|
24
26
|
*/
|
|
25
|
-
value?: string
|
|
27
|
+
value?: string | Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* What the field says while its `value` is worked out ("Finding words…").
|
|
30
|
+
*/
|
|
31
|
+
waiting?: string;
|
|
26
32
|
/**
|
|
27
33
|
* Shown while it's empty. Default none.
|
|
28
34
|
*/
|
|
29
35
|
placeholder?: string;
|
|
36
|
+
/**
|
|
37
|
+
* A `<textarea>` (cr-textarea) rather than one line: Enter starts a new
|
|
38
|
+
* line, and only the action button saves. Default false.
|
|
39
|
+
*/
|
|
40
|
+
multiline?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* A multi-line field's height in lines. Default 6.
|
|
43
|
+
*/
|
|
44
|
+
rows?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Help under the field (cr-input's
|
|
47
|
+
* description), or a function of its text, kept up as it's typed ("12 words"). Default none.
|
|
48
|
+
*/
|
|
49
|
+
hint?: string | ((text: string) => string);
|
|
30
50
|
/**
|
|
31
51
|
* Why the (non-blank) text can't be used,
|
|
32
52
|
* shown under the field; null when it can. Default: anything but blank.
|
|
@@ -71,8 +91,16 @@ export type FieldsRequest = DialogRequest & {
|
|
|
71
91
|
* @typedef {object} FieldRequest
|
|
72
92
|
* @property {string} name Its key in the answer ("folder"), and part of its id.
|
|
73
93
|
* @property {string} label Its label ("Folder").
|
|
74
|
-
* @property {string} [value] The text to start with. Default ''.
|
|
94
|
+
* @property {string | Promise<string>} [value] The text to start with. Default ''. A promise is text
|
|
95
|
+
* still being worked out (an on-device model's suggestion): the field waits, disabled, with
|
|
96
|
+
* `waiting` under it, and takes the text when it comes ('' if it fails).
|
|
97
|
+
* @property {string} [waiting] What the field says while its `value` is worked out ("Finding words…").
|
|
75
98
|
* @property {string} [placeholder] Shown while it's empty. Default none.
|
|
99
|
+
* @property {boolean} [multiline] A `<textarea>` (cr-textarea) rather than one line: Enter starts a new
|
|
100
|
+
* line, and only the action button saves. Default false.
|
|
101
|
+
* @property {number} [rows] A multi-line field's height in lines. Default 6.
|
|
102
|
+
* @property {string | ((text: string) => string)} [hint] Help under the field (cr-input's
|
|
103
|
+
* description), or a function of its text, kept up as it's typed ("12 words"). Default none.
|
|
76
104
|
* @property {(text: string) => string | null} [validate] Why the (non-blank) text can't be used,
|
|
77
105
|
* shown under the field; null when it can. Default: anything but blank.
|
|
78
106
|
* @property {number} [maxLength] Its `maxlength`. Default none.
|
|
@@ -120,13 +148,14 @@ export declare function checkText(text: string, validate?: (text: string) => str
|
|
|
120
148
|
*
|
|
121
149
|
* @param {string} name - The field's name.
|
|
122
150
|
* @param {boolean} only - Whether it's the dialog's only field.
|
|
123
|
-
* @returns {{input: string, error: string}} The ids.
|
|
151
|
+
* @returns {{input: string, error: string, hint: string}} The ids.
|
|
124
152
|
* @example
|
|
125
|
-
* fieldIds('folder', false); // → {input: 'text-dialog-folder', error: 'text-dialog-folder-error'}
|
|
153
|
+
* fieldIds('folder', false); // → {input: 'text-dialog-folder', error: 'text-dialog-folder-error', hint: 'text-dialog-folder-hint'}
|
|
126
154
|
*/
|
|
127
155
|
export declare function fieldIds(name: string, only: boolean): {
|
|
128
156
|
input: string;
|
|
129
157
|
error: string;
|
|
158
|
+
hint: string;
|
|
130
159
|
};
|
|
131
160
|
export type Parts = {
|
|
132
161
|
/**
|
|
@@ -162,11 +191,19 @@ export type Field = {
|
|
|
162
191
|
/**
|
|
163
192
|
* Its input.
|
|
164
193
|
*/
|
|
165
|
-
input: HTMLInputElement;
|
|
194
|
+
input: HTMLInputElement | HTMLTextAreaElement;
|
|
166
195
|
/**
|
|
167
196
|
* The reason under it.
|
|
168
197
|
*/
|
|
169
198
|
error: HTMLElement;
|
|
199
|
+
/**
|
|
200
|
+
* The help under it.
|
|
201
|
+
*/
|
|
202
|
+
hint: HTMLElement;
|
|
203
|
+
/**
|
|
204
|
+
* Whether its value is still being worked out.
|
|
205
|
+
*/
|
|
206
|
+
waiting: boolean;
|
|
170
207
|
};
|
|
171
208
|
/**
|
|
172
209
|
* Ask for several pieces of text in the page's dialog, one field each.
|
|
@@ -199,4 +236,4 @@ export declare function askFields({ title, fields: requests, action, cancel, ret
|
|
|
199
236
|
* validate: (text) => (siteFromInput(text) ? null : 'Enter a site, like example.com'),
|
|
200
237
|
* });
|
|
201
238
|
*/
|
|
202
|
-
export declare function askText({ label, value, placeholder, validate, maxLength, ...dialog }: TextRequest): Promise<string | null>;
|
|
239
|
+
export declare function askText({ label, value, placeholder, validate, maxLength, multiline, rows, hint, waiting, ...dialog }: TextRequest): Promise<string | null>;
|