browser-ai-summarizer 1.0.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 ADDED
@@ -0,0 +1,142 @@
1
+ ## Browser AI Summarizer
2
+
3
+ **Production-grade package for Browser's built-in [Summarizer API](https://developer.mozilla.org/en-US/docs/Web/API/Summarizer_API).**
4
+
5
+ - **Zero dependencies** – ships as a single JS bundle.
6
+ - **On-device summaries** – uses the Browser Summarizer API.
7
+ - **Drop-in UX** – smart defaults for common article/blog layouts.
8
+
9
+ ### Browser Compatibility
10
+
11
+ The Summarizer API is an experimental feature available only in **Chrome 138 and later** on desktop platforms (Windows, macOS, Linux, and ChromeOS). It is not supported on mobile devices. Before using this package, ensure your environment meets the hardware and browser requirements outlined in the [official Chrome documentation](https://developer.chrome.com/docs/ai/summarizer-api#requirements).
12
+
13
+ ### Install (npm)
14
+
15
+ ```bash
16
+ npm install browser-ai-summarizer
17
+ ```
18
+
19
+ ### Usage (ESM / bundlers)
20
+
21
+ ```ts
22
+ import { AISummarizer } from "browser-ai-summarizer";
23
+
24
+ const summarizer = new AISummarizer({
25
+ theme: { accentColor: "#0066cc" },
26
+ });
27
+
28
+ summarizer.mount();
29
+ ```
30
+
31
+ ### Usage (CommonJS)
32
+
33
+ ```js
34
+ const { AISummarizer } = require("browser-ai-summarizer");
35
+
36
+ const summarizer = new AISummarizer({
37
+ buttonLabel: "Summarize",
38
+ });
39
+
40
+ summarizer.mount();
41
+ ```
42
+
43
+ ### Usage via CDN (UMD)
44
+
45
+ ```html
46
+ <script src="https://cdn.jsdelivr.net/npm/browser-ai-summarizer/dist/browser-ai-summarizer.umd.js"></script>
47
+ <script>
48
+ // AISummarizer is exposed as a global
49
+ const widget = new AISummarizer.AISummarizer({
50
+ theme: { accentColor: "#111111" },
51
+ });
52
+ widget.mount();
53
+ </script>
54
+ ```
55
+
56
+ ### Zero-JS auto-init (data attribute)
57
+
58
+ Add a placeholder element with `data-browser-ai-summarizer`. When the script runs in the browser, it will automatically mount a widget:
59
+
60
+ ```html
61
+ <div data-browser-ai-summarizer></div>
62
+ ```
63
+
64
+ You can also pass JSON options via the attribute:
65
+
66
+ ```html
67
+ <div
68
+ data-browser-ai-summarizer='{"theme":{"accentColor":"#0066cc"},"buttonLabel":"Summarize with AI"}'
69
+ ></div>
70
+ ```
71
+
72
+ ### Options
73
+
74
+ All options are optional; these are the most important ones:
75
+
76
+ - **contentSelector** (`string`): CSS selector(s) for the article/content element. First match wins.
77
+ - **anchorSelector** (`string`): CSS selector(s) for the element after which the button is inserted.
78
+ - **defaultType** (`'key-points' | 'tldr' | 'teaser' | 'headline'`): Initial summary style.
79
+ - **defaultLength** (`'short' | 'medium' | 'long'`): Summary length hint for the API.
80
+ - **format** (`'markdown' | 'plain-text'`): Output format requested from the API.
81
+ - **preference** (`'auto' | 'speed' | 'capability'`): Preference hint for model quality vs speed.
82
+ - **sharedContext** (`string`): Extra context string passed to the summarizer.
83
+ - **outputLanguage** (`string`): Preferred language for generated summaries. The Summarizer API attempts to match this language, but it is not guaranteed.
84
+ - **expectedInputLanguages** (`string[]`): Expected language(s) of the page/article text.
85
+ - **expectedContextLanguages** (`string[]`): Expected language(s) of your `sharedContext`.
86
+ - **buttonLabel** (`string`): Label text for the trigger button.
87
+ - **showTypeSwitcher** (`boolean`): Whether to show style-switcher pills.
88
+ - **requireDownloadConsent** (`boolean`): Whether to ask before downloading the model.
89
+ - **dbName** (`string`): Name of the IndexedDB database for user prefs.
90
+ - **theme** (`AISummarizerTheme`): Theme overrides mapped to CSS custom properties.
91
+
92
+ ### Availability behavior
93
+
94
+ Before showing unsupported/download states, the widget checks `Summarizer.availability(...)`
95
+ with the same creation profile it intends to use (`type`, `length`, `format`, `preference`,
96
+ `sharedContext`, and language settings).
97
+ Chrome may report different availability results depending on the option profile,
98
+ so this avoids false positives/negatives between preflight checks and `Summarizer.create(...)`.
99
+ The availability probe is side-effect free and does not pass `monitor` or `signal`.
100
+
101
+ ### Multilingual configuration examples
102
+
103
+ Use language tags when your source text and desired output language differ:
104
+
105
+ ```ts
106
+ import { AISummarizer } from "browser-ai-summarizer";
107
+
108
+ new AISummarizer({
109
+ // Spanish article -> English summary
110
+ expectedInputLanguages: ["es"],
111
+ outputLanguage: "en",
112
+ }).mount();
113
+ ```
114
+
115
+ ```ts
116
+ import { AISummarizer } from "browser-ai-summarizer";
117
+
118
+ new AISummarizer({
119
+ // Japanese article + English context -> Japanese summary
120
+ expectedInputLanguages: ["ja"],
121
+ expectedContextLanguages: ["en"],
122
+ sharedContext: "Summarize for product managers and keep key metrics.",
123
+ outputLanguage: "ja",
124
+ }).mount();
125
+ ```
126
+
127
+ > For best predictability, evaluate `Summarizer.availability(...)` and `Summarizer.create(...)` with the same option profile (type/length/context/language settings).
128
+
129
+ ### Theme tokens (`AISummarizerTheme`)
130
+
131
+ All theme values are optional:
132
+
133
+ - **buttonBg**: Button background color.
134
+ - **buttonColor**: Button text color.
135
+ - **buttonRadius**: Button border-radius.
136
+ - **buttonFont**: Button font-family.
137
+ - **accentColor**: Card accent / border color.
138
+ - **cardBg**: Card background color.
139
+ - **cardHeaderBg**: Card header background.
140
+ - **cardBorder**: Card border color.
141
+ - **cardFont**: Summary text font-family.
142
+ - **zIndex**: z-index for all plugin elements.
@@ -0,0 +1,408 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=`ai-summarizer-styles`;function t(t){if(document.getElementById(e))return;let r=document.createElement(`style`);r.id=e,r.textContent=n(t),document.head.appendChild(r)}function n(e){let t={buttonBg:e.buttonBg??`#111111`,buttonColor:e.buttonColor??`#f7f3ed`,buttonRadius:e.buttonRadius??`3px`,buttonFont:e.buttonFont??`system-ui, -apple-system, sans-serif`,accentColor:e.accentColor??`#111111`,cardBg:e.cardBg??`#faf8f5`,cardHeaderBg:e.cardHeaderBg??`#f3ede5`,cardBorder:e.cardBorder??`#e4ddd4`,cardFont:e.cardFont??`system-ui, -apple-system, sans-serif`,zIndex:e.zIndex??1};return`
2
+ /* ── AI Summarizer Widget ────────────────────────────────── */
3
+ .ais-root {
4
+ --ais-btn-bg: ${t.buttonBg};
5
+ --ais-btn-color: ${t.buttonColor};
6
+ --ais-btn-radius: ${t.buttonRadius};
7
+ --ais-btn-font: ${t.buttonFont};
8
+ --ais-accent: ${t.accentColor};
9
+ --ais-card-bg: ${t.cardBg};
10
+ --ais-card-head-bg: ${t.cardHeaderBg};
11
+ --ais-card-border: ${t.cardBorder};
12
+ --ais-card-font: ${t.cardFont};
13
+ --ais-z: ${t.zIndex};
14
+ }
15
+
16
+ /* ── Trigger ── */
17
+ .ais-trigger {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 0.75rem;
21
+ margin: 1.75rem 0 0;
22
+ flex-wrap: wrap;
23
+ position: relative;
24
+ z-index: var(--ais-z);
25
+ }
26
+
27
+ .ais-btn {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ gap: 0.45rem;
31
+ padding: 0.55rem 1.15rem;
32
+ background: var(--ais-btn-bg);
33
+ color: var(--ais-btn-color);
34
+ font-family: var(--ais-btn-font);
35
+ font-size: 0.8rem;
36
+ font-weight: 500;
37
+ letter-spacing: 0.05em;
38
+ text-transform: uppercase;
39
+ border: 1.5px solid var(--ais-btn-bg);
40
+ cursor: pointer;
41
+ border-radius: var(--ais-btn-radius);
42
+ line-height: 1;
43
+ transition: filter 0.15s, opacity 0.2s, transform 0.1s;
44
+ user-select: none;
45
+ }
46
+ .ais-btn:hover:not(:disabled) { filter: brightness(1.15); }
47
+ .ais-btn:active:not(:disabled) { transform: scale(0.97); }
48
+ .ais-btn:disabled { opacity: 0.4; cursor: not-allowed; }
49
+ .ais-btn svg { width: 14px; height: 14px; display: block; }
50
+
51
+ .ais-btn--ghost {
52
+ background: transparent;
53
+ color: #666;
54
+ border-color: #d5cfc7;
55
+ }
56
+ .ais-btn--ghost:hover:not(:disabled) { filter: none; background: #f5f5f5; }
57
+
58
+ /* ── Spinner ── */
59
+ .ais-spinner {
60
+ display: inline-block;
61
+ width: 1em;
62
+ height: 1em;
63
+ border: 0.15em solid rgba(255,255,255,0.2);
64
+ border-top-color: currentColor;
65
+ border-radius: 50%;
66
+ animation: ais-spin 0.65s linear infinite;
67
+ flex-shrink: 0;
68
+ vertical-align: -0.125em;
69
+ box-sizing: border-box;
70
+ }
71
+ .ais-spinner--dark {
72
+ border-color: rgba(0,0,0,0.12);
73
+ border-top-color: #333;
74
+ }
75
+ @keyframes ais-spin { to { transform: rotate(360deg); } }
76
+
77
+ /* ── Type pills ── */
78
+ .ais-pills {
79
+ display: none;
80
+ align-items: center;
81
+ gap: 0.35rem;
82
+ flex-wrap: wrap;
83
+ margin: 0.75rem 0 0;
84
+ }
85
+ .ais-pills--visible { display: flex; }
86
+ .ais-pills__label {
87
+ font-family: var(--ais-btn-font);
88
+ font-size: 0.7rem;
89
+ color: #999;
90
+ letter-spacing: 0.06em;
91
+ text-transform: uppercase;
92
+ margin-right: 0.15rem;
93
+ }
94
+ .ais-pill {
95
+ font-family: var(--ais-btn-font);
96
+ font-size: 0.7rem;
97
+ font-weight: 500;
98
+ letter-spacing: 0.05em;
99
+ text-transform: uppercase;
100
+ padding: 0.22rem 0.6rem;
101
+ border: 1px solid #ddd;
102
+ border-radius: 100px;
103
+ background: #fff;
104
+ color: #777;
105
+ cursor: pointer;
106
+ transition: all 0.13s;
107
+ line-height: 1;
108
+ }
109
+ .ais-pill:hover:not(:disabled):not(.ais-pill--active) { border-color: #999; color: #333; }
110
+ .ais-pill:disabled {
111
+ opacity: 0.5;
112
+ cursor: not-allowed;
113
+ filter: grayscale(0.5);
114
+ }
115
+ .ais-pill--active {
116
+ background: var(--ais-accent);
117
+ border-color: var(--ais-accent);
118
+ color: #fff;
119
+ }
120
+
121
+ /* ── Consent dialog ── */
122
+ .ais-consent {
123
+ display: none;
124
+ margin: 1.1rem 0 0;
125
+ padding: 1.1rem 1.4rem;
126
+ background: #fffdf9;
127
+ border: 1px solid #e8e3db;
128
+ border-left: 3px solid #c8a96a;
129
+ border-radius: 0 4px 4px 0;
130
+ font-family: var(--ais-card-font);
131
+ animation: ais-fade-up 0.25s ease;
132
+ }
133
+ .ais-consent--visible { display: block; }
134
+ .ais-consent__title {
135
+ font-size: 0.82rem;
136
+ font-weight: 600;
137
+ color: #111;
138
+ margin: 0 0 0.4rem;
139
+ }
140
+ .ais-consent__body {
141
+ font-size: 0.82rem;
142
+ color: #555;
143
+ line-height: 1.65;
144
+ margin: 0 0 0.9rem;
145
+ }
146
+ .ais-consent__body strong { color: #333; font-weight: 600; }
147
+ .ais-consent__actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
148
+
149
+ /* ── Download progress ── */
150
+ .ais-progress {
151
+ display: none;
152
+ margin: 1.1rem 0 0;
153
+ padding: 0.9rem 1.1rem;
154
+ background: var(--ais-card-bg);
155
+ border: 1px solid var(--ais-card-border);
156
+ border-radius: 4px;
157
+ font-family: var(--ais-card-font);
158
+ }
159
+ .ais-progress--visible { display: block; }
160
+ .ais-progress__top {
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: space-between;
164
+ margin-bottom: 0.5rem;
165
+ gap: 0.5rem;
166
+ }
167
+ .ais-progress__label {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 0.45rem;
171
+ font-size: 0.8rem;
172
+ color: #555;
173
+ }
174
+ .ais-progress__pct {
175
+ font-size: 0.78rem;
176
+ font-weight: 600;
177
+ color: #111;
178
+ font-variant-numeric: tabular-nums;
179
+ }
180
+ .ais-progress__bar-bg {
181
+ height: 3px;
182
+ background: #e0dbd3;
183
+ border-radius: 2px;
184
+ overflow: hidden;
185
+ }
186
+ .ais-progress__bar {
187
+ height: 100%;
188
+ background: var(--ais-accent);
189
+ border-radius: 2px;
190
+ width: 0%;
191
+ transition: width 0.25s ease;
192
+ }
193
+ .ais-progress__size {
194
+ margin-top: 0.45rem;
195
+ font-size: 0.72rem;
196
+ color: #666;
197
+ font-variant-numeric: tabular-nums;
198
+ }
199
+ .ais-progress__note { margin-top: 0.35rem; font-size: 0.72rem; color: #aaa; }
200
+
201
+ /* ── Summary card ── */
202
+ .ais-card {
203
+ margin: 1.5rem 0 0;
204
+ border: 1px solid var(--ais-card-border);
205
+ border-left: 3px solid var(--ais-accent);
206
+ background: var(--ais-card-bg);
207
+ border-radius: 0 4px 4px 0;
208
+ overflow: hidden;
209
+ font-family: var(--ais-card-font);
210
+ animation: ais-fade-up 0.35s ease forwards;
211
+ }
212
+ @keyframes ais-fade-up {
213
+ from { opacity: 0; transform: translateY(5px); }
214
+ to { opacity: 1; transform: translateY(0); }
215
+ }
216
+
217
+ .ais-card__head {
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: space-between;
221
+ padding: 0.8rem 1.2rem;
222
+ border-bottom: 1px solid var(--ais-card-border);
223
+ background: var(--ais-card-head-bg);
224
+ gap: 0.5rem;
225
+ flex-wrap: wrap;
226
+ }
227
+ .ais-card__title-row { display: flex; align-items: baseline; gap: 0.55rem; }
228
+ .ais-card__title {
229
+ font-size: 0.9rem;
230
+ font-weight: 600;
231
+ color: #111;
232
+ margin: 0;
233
+ font-family: var(--ais-card-font);
234
+ }
235
+ .ais-card__badge {
236
+ font-size: 0.63rem;
237
+ font-weight: 500;
238
+ letter-spacing: 0.08em;
239
+ text-transform: uppercase;
240
+ color: #888;
241
+ border: 1px solid #d5cfc7;
242
+ padding: 0.1rem 0.38rem;
243
+ border-radius: 2px;
244
+ background: #fff;
245
+ white-space: nowrap;
246
+ }
247
+ .ais-card__actions { display: flex; gap: 0.25rem; align-items: center; }
248
+ .ais-icon-btn {
249
+ background: none;
250
+ border: none;
251
+ cursor: pointer;
252
+ padding: 0.28rem;
253
+ color: #aaa;
254
+ border-radius: 3px;
255
+ line-height: 0;
256
+ transition: color 0.13s, background 0.13s;
257
+ }
258
+ .ais-icon-btn:hover { color: #333; background: rgba(0,0,0,0.06); }
259
+ .ais-icon-btn svg { width: 14px; height: 14px; display: block; }
260
+
261
+ .ais-card__body { padding: 1.1rem 1.4rem 0.9rem; }
262
+ .ais-card__content {
263
+ font-size: 0.89rem;
264
+ color: #2a2a2a;
265
+ line-height: 1.78;
266
+ min-height: 1.5rem;
267
+ }
268
+ .ais-card__content ul {
269
+ list-style: none;
270
+ padding: 0; margin: 0;
271
+ display: flex;
272
+ flex-direction: column;
273
+ gap: 0.4rem;
274
+ }
275
+ .ais-card__content li {
276
+ display: flex;
277
+ align-items: flex-start;
278
+ gap: 0.6rem;
279
+ padding: 0.5rem 0.75rem;
280
+ background: #fff;
281
+ border: 1px solid #ece7df;
282
+ border-radius: 3px;
283
+ font-size: 0.875rem;
284
+ line-height: 1.65;
285
+ color: #333;
286
+ }
287
+ .ais-card__content li::before {
288
+ content: '›';
289
+ color: #999;
290
+ font-size: 1.05rem;
291
+ line-height: 1.45;
292
+ flex-shrink: 0;
293
+ }
294
+ .ais-card__content li span {
295
+ flex: 1;
296
+ }
297
+ .ais-card__content p { margin: 0 0 0.6rem; }
298
+ .ais-card__content p:last-child { margin: 0; }
299
+ .ais-card__content strong { font-weight: 600; color: #111; }
300
+
301
+ /* Streaming cursor */
302
+ .ais-cursor {
303
+ display: inline-block;
304
+ width: 2px; height: 0.85em;
305
+ background: #666;
306
+ vertical-align: text-bottom;
307
+ margin-left: 2px;
308
+ animation: ais-blink 0.85s step-end infinite;
309
+ }
310
+ @keyframes ais-blink { 50% { opacity: 0; } }
311
+
312
+ .ais-card__foot {
313
+ display: flex;
314
+ align-items: center;
315
+ justify-content: space-between;
316
+ padding: 0.55rem 1.4rem;
317
+ border-top: 1px solid #ece7df;
318
+ gap: 0.5rem;
319
+ flex-wrap: wrap;
320
+ }
321
+ .ais-card__note {
322
+ font-size: 0.68rem;
323
+ color: #bbb;
324
+ letter-spacing: 0.03em;
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 0.35rem;
328
+ }
329
+ .ais-card__note-dot {
330
+ width: 3px; height: 3px;
331
+ background: #ccc; border-radius: 50%; flex-shrink: 0;
332
+ }
333
+
334
+ /* ── Error ── */
335
+ .ais-error {
336
+ margin: 1.1rem 0 0;
337
+ padding: 0.9rem 1.1rem;
338
+ background: #fff8f7;
339
+ border: 1px solid #f0cac7;
340
+ border-left: 3px solid #b94040;
341
+ border-radius: 0 4px 4px 0;
342
+ font-family: var(--ais-card-font);
343
+ font-size: 0.82rem;
344
+ color: #7a2a2a;
345
+ display: flex;
346
+ align-items: flex-start;
347
+ justify-content: space-between;
348
+ gap: 0.75rem;
349
+ animation: ais-fade-up 0.25s ease;
350
+ }
351
+ .ais-error a { color: inherit; }
352
+ .ais-error__retry {
353
+ font-family: var(--ais-btn-font);
354
+ font-size: 0.76rem;
355
+ font-weight: 500;
356
+ color: #7a2a2a;
357
+ background: none;
358
+ border: 1px solid #f0cac7;
359
+ border-radius: 3px;
360
+ padding: 0.28rem 0.65rem;
361
+ cursor: pointer;
362
+ white-space: nowrap;
363
+ flex-shrink: 0;
364
+ transition: background 0.13s;
365
+ }
366
+ .ais-error__retry:hover { background: rgba(0,0,0,0.04); }
367
+ `}var r=`preferences`,i=`userPrefs`;function a(e){return new Promise((t,n)=>{let i=indexedDB.open(e,1);i.onupgradeneeded=e=>{e.target.result.createObjectStore(r)},i.onsuccess=e=>t(e.target.result),i.onerror=()=>n(i.error)})}async function o(e){try{let t=(await a(e)).transaction(r,`readonly`).objectStore(r).get(i);return await new Promise(e=>{t.onsuccess=()=>e(t.result??null),t.onerror=()=>e(null)})}catch{return null}}async function s(e,t){try{(await a(e)).transaction(r,`readwrite`).objectStore(r).put(t,i)}catch{}}var c=3.5,l=8e3;function u(){let e=globalThis.Summarizer;if(!e)throw Error(`Summarizer API is not available in this browser.`);return e}function d(){return`Summarizer`in globalThis}async function f(e={}){return u().availability({...e,expectedInputLanguages:b(e.expectedInputLanguages),expectedContextLanguages:b(e.expectedContextLanguages)})}var p=null,m=null;function h(e){return JSON.stringify({type:e.type,length:e.length,format:e.format,preference:e.preference,sharedContext:e.sharedContext,outputLanguage:e.outputLanguage,expectedInputLanguages:e.expectedInputLanguages,expectedContextLanguages:e.expectedContextLanguages})}async function g(e){let t=h(e);return p&&m===t?p:(p&&=((await p).destroy(),null),m=t,p=_(e),p)}async function _(e){return await f(e)!==`available`&&x(),u().create({type:e.type,length:e.length,format:e.format??`markdown`,preference:e.preference,sharedContext:e.sharedContext,outputLanguage:e.outputLanguage,expectedInputLanguages:b(e.expectedInputLanguages),expectedContextLanguages:b(e.expectedContextLanguages),signal:e.signal,monitor:e.onDownloadProgress?t=>{t.addEventListener(`downloadprogress`,t=>{let n=t,r=typeof n.loaded==`number`?n.loaded:0,i=typeof n.total==`number`?n.total:0,a=!!n.lengthComputable,o=a&&i>0?Math.round(r/i*100):r<=1?Math.round(r*100):0;e.onDownloadProgress?.({pct:o,loaded:r,total:i,lengthComputable:a})})}:void 0})}function v(e,t){let n=t?Math.floor(t*c):l;if(e.length<=n)return e;let r=e.lastIndexOf(`
368
+
369
+ `,n),i=r>n*.6?r:n;return e.slice(0,i)+`
370
+
371
+ [… content truncated for summary]`}async function y(e){let{summarizer:t,context:n,signal:r,onChunk:i}=e,a=new TextDecoder,o=e=>{if(typeof e==`string`)return e;if(e instanceof Uint8Array)return a.decode(e);if(e instanceof ArrayBuffer)return a.decode(new Uint8Array(e));if(e&&typeof e==`object`){let t=e.text;if(typeof t==`string`)return t;let n=e.output;if(typeof n==`string`)return n;let r=e.outputText;if(typeof r==`string`)return r}return null},s=await S(e.text,t,n,r),c=async e=>{let a=t.summarizeStreaming(e,{context:n,signal:r}),s=``,c=!1;for await(let e of C(a)){if(r.aborted)break;let t=o(e);t&&(t.startsWith(s)?s=t:s+=t,c=!0,i(s))}if(!c&&t.summarize){let a=await t.summarize(e,{context:n,signal:r});return a&&i(a),a}return s};try{return await c(s)}catch(e){if(e.name===`QuotaExceededError`){let e=t.inputQuota?Math.floor(t.inputQuota*.7):void 0;return s=v(s,e),await c(s)}throw e}}function b(e){return e&&e.length>0?e:void 0}function x(){let e=globalThis.navigator;if(e?.userActivation&&!e.userActivation.isActive)throw Error(`Summarizer.create() must be called from a user gesture.`)}async function S(e,t,n,r){let i=t.inputQuota;if(!i)return v(e,void 0);if(t.measureInputUsage)try{if(await t.measureInputUsage(e,{context:n,signal:r})<=i)return e}catch{}return v(e,i)}async function*C(e){if(e&&typeof e[Symbol.asyncIterator]==`function`){yield*e;return}if(e&&typeof e.getReader==`function`){let t=e.getReader();try{for(;;){let{value:e,done:n}=await t.read();if(n)break;yield e}}finally{t.releaseLock()}return}throw Error(`Unsupported summary stream type.`)}function w(e){return e.replace(/[&<>"']/g,e=>{switch(e){case`&`:return`&amp;`;case`<`:return`&lt;`;case`>`:return`&gt;`;case`"`:return`&quot;`;case`'`:return`&#39;`;default:return e}})}function T(e){let t=e.trim().split(`
372
+ `),n=``,r=!1;for(let e of t){let t=e.trim();if(!t){r&&=(n+=`</ul>`,!1);continue}let i=/^[-*•]\s+/.test(t),a=/^\d+\.\s+/.test(t);if(i||a){r||=(n+=`<ul>`,!0);let e=t.replace(/^([-*•]|\d+\.)\s+/,``);n+=`<li><span>${E(w(e))}</span></li>`}else r&&=(n+=`</ul>`,!1),n+=`<p>${E(w(t))}</p>`}return r&&(n+=`</ul>`),n}function E(e){return e.replace(/\*\*(.*?)\*\*/g,`<strong>$1</strong>`).replace(/__(.*?)__/g,`<strong>$1</strong>`).replace(/\*(.*?)\*/g,`<em>$1</em>`).replace(/_(.*?)_/g,`<em>$1</em>`)}function D(e,t,n){let r=document.createElement(e);return r.className=t,n!==void 0&&(r.innerHTML=n),r}function O(e){for(let t of e.split(`,`).map(e=>e.trim())){let e=document.querySelector(t);if(e)return e}return null}function k(e){let t=(O(e)??document.querySelector(`article`)??document.querySelector(`main`)??document.body).cloneNode(!0);return t.querySelectorAll(`script, style, noscript, iframe, .ads, .advertisement, nav, footer, header, aside:not(.article-content), .social-share, .comments, .related-posts`).forEach(e=>e.remove()),(t.innerText??``).replace(/[\t ]+/g,` `).replace(/\n\s*\n\s*\n+/g,`
373
+
374
+ `).trim()}function A(e,t){return t===`plain-text`?w(e).replace(/\n/g,`<br>`):T(e)}var j=`<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="width: 1.2em; height: 1.2em; vertical-align: middle;">
375
+ <path d="M7 2c.4 3.2 2 5.2 5.6 5.6-3.6.4-5.2 2-5.6 5.6-.4-3.6-2.4-5.6-6-6 4.4-.4 6-2 6.4-5.2z" fill="currentColor"/>
376
+ <path d="M13.5 1.5c.2 1.2.8 1.8 2 2-1.2.2-1.8.8-2 2-.2-1.2-.8-1.8-2-2 1.2-.2 1.8-.8 2-2z" fill="currentColor"/>
377
+ </svg>`,M=`<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
378
+ <rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/>
379
+ <path d="M3 11H2.5A1.5 1.5 0 0 1 1 9.5v-7A1.5 1.5 0 0 1 2.5 1h7A1.5 1.5 0 0 1 11 2.5V3"
380
+ stroke="currentColor" stroke-width="1.3"/>
381
+ </svg>`,N=`<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
382
+ <path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
383
+ </svg>`,ee=`<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
384
+ <rect x="3" y="3" width="10" height="10" rx="1.5" fill="currentColor"/>
385
+ </svg>`,te=`<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
386
+ <path d="M2.5 9.5v2a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-2M8.5 2v7M5.5 6l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
387
+ </svg>`,P={"key-points":`Key Points`,tldr:`TL;DR`,teaser:`Teaser`,headline:`Headline`};function F(){let e=document.createElement(`div`);return e.className=`ais-root`,e}function I(e){let t=D(`div`,`ais-trigger`),n=D(`button`,`ais-btn`);return n.setAttribute(`aria-label`,`Summarize this article with on-device AI`),n.innerHTML=`${j} ${e}`,t.appendChild(n),{wrap:t,btn:n}}function L(e,t){e.disabled=!0,e.innerHTML=`<span class="ais-spinner" aria-hidden="true"></span> ${t}`}function R(e,t=`Regenerate`){e.disabled=!1,e.innerHTML=`${j} ${t}`}function z(e,t){let n=D(`div`,`ais-pills`);n.setAttribute(`role`,`group`),n.setAttribute(`aria-label`,`Summary style`);let r=D(`span`,`ais-pills__label`);r.textContent=`Style`,n.appendChild(r);let i=[];return Object.entries(P).forEach(([r,a])=>{let o=D(`button`,`ais-pill${r===e?` ais-pill--active`:``}`);o.textContent=a,o.dataset.type=r,o.addEventListener(`click`,()=>{o.classList.contains(`ais-pill--active`)||o.disabled||(n.querySelectorAll(`.ais-pill`).forEach(e=>e.classList.remove(`ais-pill--active`)),o.classList.add(`ais-pill--active`),t(r))}),n.appendChild(o),i.push(o)}),{wrap:n,setDisabled:e=>{i.forEach(t=>{t.disabled=e,t.setAttribute(`aria-disabled`,String(e))})}}}function B(e){e.classList.add(`ais-pills--visible`)}function V(e,t){let n=D(`div`,`ais-consent`);n.setAttribute(`role`,`dialog`),n.setAttribute(`aria-modal`,`false`),n.setAttribute(`aria-labelledby`,`ais-consent-title`),n.innerHTML=`
388
+ <div class="ais-consent__title" id="ais-consent-title">⬇ Download AI model to continue</div>
389
+ <div class="ais-consent__body">
390
+ Summaries run <strong>entirely on your device</strong> using Gemini Nano — no content
391
+ leaves your browser. Browser needs to download this model once
392
+ (<strong>~1–2 GB</strong>), then it's cached for all future use.
393
+ </div>
394
+ <div class="ais-consent__actions"></div>
395
+ `;let r=n.querySelector(`.ais-consent__actions`),i=D(`button`,`ais-btn`);i.innerHTML=`${te} Download &amp; summarize`,i.addEventListener(`click`,e);let a=D(`button`,`ais-btn ais-btn--ghost`);return a.textContent=`Not now`,a.addEventListener(`click`,t),r.appendChild(i),r.appendChild(a),n}function H(e){e.classList.add(`ais-consent--visible`)}function U(e){e.classList.remove(`ais-consent--visible`)}function W(){let e=D(`div`,`ais-progress`);e.setAttribute(`role`,`status`),e.setAttribute(`aria-live`,`polite`),e.innerHTML=`
396
+ <div class="ais-progress__top">
397
+ <div class="ais-progress__label">
398
+ <span class="ais-spinner ais-spinner--dark" aria-hidden="true"></span>
399
+ Downloading Gemini Nano…
400
+ </div>
401
+ <span class="ais-progress__pct">0%</span>
402
+ </div>
403
+ <div class="ais-progress__bar-bg">
404
+ <div class="ais-progress__bar"></div>
405
+ </div>
406
+ <div class="ais-progress__size">Exact size hidden by the browser</div>
407
+ <div class="ais-progress__note">One-time download · cached in Browser for future use</div>
408
+ `;let t=e.querySelector(`.ais-progress__bar`),n=e.querySelector(`.ais-progress__pct`),r=e.querySelector(`.ais-progress__size`),i=e=>{if(!Number.isFinite(e)||e<=0)return`0 B`;let t=[`B`,`KB`,`MB`,`GB`,`TB`],n=e,r=0;for(;n>=1024&&r<t.length-1;)n/=1024,r+=1;let i=n>=100?0:n>=10?1:2;return`${n.toFixed(i)} ${t[r]}`};return{wrap:e,setProgress:a=>{let o=Math.max(0,Math.min(100,Math.round(a.pct)));t.style.width=`${o}%`,n.textContent=`${o}%`,a.lengthComputable&&a.total>1?r.textContent=`${i(Math.min(a.loaded,a.total))} / ${i(a.total)}`:a.loaded>1?r.textContent=`${i(a.loaded)} downloaded`:r.textContent=`Exact size hidden by the browser`,e.classList.add(`ais-progress--visible`)}}}function G(e){e.classList.remove(`ais-progress--visible`)}function K(e,t,n){let r=D(`div`,`ais-card`);r.id=`ais-card`,r.setAttribute(`role`,`region`),r.setAttribute(`aria-label`,`AI-generated article summary`);let i=D(`div`,`ais-card__head`),a=D(`div`,`ais-card__title-row`),o=D(`h2`,`ais-card__title`);o.textContent=`AI Summary`;let s=D(`span`,`ais-card__badge`);s.textContent=`${P[e]} · ${t}`,a.appendChild(o),a.appendChild(s);let c=D(`div`,`ais-card__actions`),l=D(`button`,`ais-icon-btn`);l.setAttribute(`title`,`Copy summary`),l.setAttribute(`aria-label`,`Copy summary to clipboard`),l.innerHTML=M;let u=D(`button`,`ais-icon-btn ais-stop-btn`);u.setAttribute(`title`,`Stop generating`),u.setAttribute(`aria-label`,`Stop generating summary`),u.innerHTML=ee;let d=D(`button`,`ais-icon-btn`);d.setAttribute(`title`,`Dismiss`),d.setAttribute(`aria-label`,`Dismiss summary`),d.innerHTML=N,c.appendChild(u),c.appendChild(l),c.appendChild(d),i.appendChild(a),i.appendChild(c);let f=D(`div`,`ais-card__body`),p=D(`div`,`ais-card__content`);p.innerHTML=`<span class="ais-cursor" aria-hidden="true"></span>`,f.appendChild(p);let m=D(`div`,`ais-card__foot`),h=D(`span`,`ais-card__note`);h.innerHTML=`<span class="ais-card__note-dot" aria-hidden="true"></span>On-device · Gemini Nano · Browser Summarizer API`,m.appendChild(h),r.appendChild(i),r.appendChild(f),r.appendChild(m);let g=``;l.addEventListener(`click`,async()=>{if(g)try{await navigator.clipboard.writeText(g),l.innerHTML=`✓`,l.style.color=`#4a9a5c`,setTimeout(()=>{l.innerHTML=M,l.style.color=``},2e3)}catch{}}),d.addEventListener(`click`,()=>r.remove());let _=e=>{let t=p.querySelector(`.ais-cursor`);e&&!t&&p.insertAdjacentHTML(`beforeend`,`<span class="ais-cursor" aria-hidden="true"></span>`),e||t?.remove()};return{card:r,contentEl:p,stopBtn:u,setCursor:_,setContent:e=>{g=e,p.innerHTML=A(e,n),_(!1)},streamChunk:e=>{g=e,p.innerHTML=A(e,n),_(!1)},finalize:()=>{_(!1),u.hidden=!0}}}function q(e,t){let n=D(`div`,`ais-error`);n.id=`ais-error`,n.setAttribute(`role`,`alert`);let r=D(`span`,``);if(r.innerHTML=`⚠ ${e}`,n.appendChild(r),t){let e=D(`button`,`ais-error__retry`);e.textContent=`Retry`,e.addEventListener(`click`,()=>{n.remove(),t()}),n.appendChild(e)}return n}var J=`.gh-content, .post-content, .article-body, article .content, .entry-content`,Y=`.gh-article-header, .post-header, .article-header, header.article`,X=`This is a news article or blog post. Avoid jargon, use correct grammar, focus on clarity, and ensure the reader can grasp the core ideas quickly.`,Z=new Set([`en`,`es`,`ja`]);function ne(e){let t=e?.trim().toLowerCase();if(!t)return`en`;let n=t.split(`-`)[0];return Z.has(n)?n:`en`}var Q=class{opts;abortController=null;root=null;mounted=!1;isSummarizing=!1;constructor(e={}){this.opts={contentSelector:e.contentSelector??J,anchorSelector:e.anchorSelector??Y,defaultType:e.defaultType??`key-points`,defaultLength:e.defaultLength??`medium`,format:e.format??`markdown`,preference:e.preference??`auto`,sharedContext:e.sharedContext??X,outputLanguage:ne(e.outputLanguage),expectedInputLanguages:e.expectedInputLanguages??[],expectedContextLanguages:e.expectedContextLanguages??[],buttonLabel:e.buttonLabel??`Summarize with AI`,showTypeSwitcher:e.showTypeSwitcher??!0,requireDownloadConsent:e.requireDownloadConsent??!0,dbName:e.dbName??`ai-summarizer`,theme:e.theme??{},onSummary:e.onSummary,onError:e.onError,onUnsupported:e.onUnsupported}}async mount(){if(this.mounted)return;if(this.mounted=!0,!d()){this.opts.onUnsupported?.();return}t(this.opts.theme);let e={type:(await o(this.opts.dbName))?.type??this.opts.defaultType},n=O(this.opts.anchorSelector),r=O(this.opts.contentSelector)??document.querySelector(`article`)??document.querySelector(`main`);if(!r)return;this.root=F();let i=this.buildUI(e);this.root.appendChild(i.triggerWrap),i.pillsEl&&this.root.appendChild(i.pillsEl),this.root.appendChild(i.consentEl),this.root.appendChild(i.progressEl),i.triggerBtn.addEventListener(`click`,async()=>{this.clearSummary(),U(i.consentEl),await this.preflightAndRun(e,i,!1)}),n?n.insertAdjacentElement(`afterend`,this.root):r.insertAdjacentElement(`beforebegin`,this.root),this.warmup(e).catch(()=>{})}async warmup(e){if(!d())return;let{expectedInputLanguages:t,expectedContextLanguages:n}=this.getLanguageHints();try{await g({type:e?.type??this.opts.defaultType,length:this.opts.defaultLength,format:this.opts.format,preference:this.opts.preference,sharedContext:this.opts.sharedContext,outputLanguage:this.opts.outputLanguage,expectedInputLanguages:t,expectedContextLanguages:n})}catch{}}buildUI(e){let{wrap:t,btn:n}=I(this.opts.buttonLabel),{wrap:r,setProgress:i}=W(),a={},o=null,c=()=>{};if(this.opts.showTypeSwitcher){let{wrap:t,setDisabled:n}=z(e.type,async t=>{this.isSummarizing||(e.type=t,await s(this.opts.dbName,e),this.clearSummary(),await this.preflightAndRun(e,a,!1))});o=t,c=n}let l=V(async()=>{U(l),await this.preflightAndRun(e,a,!0)},()=>U(l));return a.triggerWrap=t,a.triggerBtn=n,a.pillsEl=o,a.setPillsDisabled=c,a.progressEl=r,a.progressSetProgress=i,a.consentEl=l,a}setSummarizingState(e,t){this.isSummarizing=t,e.setPillsDisabled(t)}async summarize(){this.root||await this.mount(),this.root?.querySelector(`.ais-btn`)?.click()}destroy(){this.abortController?.abort(),this.root?.remove(),this.root=null,this.mounted=!1}clearSummary(){this.root?.querySelector(`#ais-card`)?.remove(),this.root?.querySelector(`#ais-error`)?.remove()}showError(e,t){this.clearSummary();let n=q(e,t);this.root?.appendChild(n)}getLanguageHints(){return{expectedInputLanguages:this.opts.expectedInputLanguages?.length?this.opts.expectedInputLanguages:void 0,expectedContextLanguages:this.opts.expectedContextLanguages?.length?this.opts.expectedContextLanguages:void 0}}async runSummarizer(e,t,n,r){let{triggerBtn:i,pillsEl:a,progressEl:o,progressSetProgress:c,consentEl:l}=t,{expectedInputLanguages:u,expectedContextLanguages:d}=this.getLanguageHints();this.abortController?.abort(),this.abortController=new AbortController;let f=this.abortController.signal;this.setSummarizingState(t,!0),L(i,`Preparing…`),this.clearSummary(),U(l),G(o);let p=n===`downloadable`||n===`downloading`,m=null,h=null;try{p||(h=setTimeout(()=>{f.aborted||L(i,`Loading model…`)},1200)),m=await g({type:e.type,length:this.opts.defaultLength,format:this.opts.format,preference:this.opts.preference,sharedContext:this.opts.sharedContext,outputLanguage:this.opts.outputLanguage,expectedInputLanguages:u,expectedContextLanguages:d,signal:f,onDownloadProgress:p?e=>{c(e),L(i,`Downloading… ${e.pct}%`)}:void 0}),h!==null&&(clearTimeout(h),h=null),G(o),L(i,`Summarizing…`);let n=k(this.opts.contentSelector);if(!n||n.length<80)throw Error(`Not enough article text found to summarize.`);let r=K(e.type,this.opts.defaultLength,this.opts.format);this.root?.appendChild(r.card),r.card.scrollIntoView({behavior:`smooth`,block:`nearest`}),r.stopBtn.addEventListener(`click`,()=>{this.abortController?.abort(),r.finalize(),this.setSummarizingState(t,!1),R(i,this.opts.buttonLabel)});let l=document.title??``,_=await y({summarizer:m,text:n,context:`Article title: "${l}". Provide a clear, accurate summary.`,signal:f,onChunk:e=>r.streamChunk(e)});r.finalize(),a&&B(a),R(i,`Regenerate`),_&&this.opts.onSummary?.(_,e.type),await s(this.opts.dbName,e)}catch(n){if(n.name===`AbortError`){R(i,this.opts.buttonLabel);return}G(o);let a=n instanceof Error?n:Error(String(n));this.opts.onError?.(a),this.showError(`Summary failed: ${a.message||`Unknown error.`}`,()=>this.preflightAndRun(e,t,r)),R(i,this.opts.buttonLabel)}finally{h!==null&&clearTimeout(h),this.setSummarizingState(t,!1)}}async preflightAndRun(e,t,n){let{expectedInputLanguages:r,expectedContextLanguages:i}=this.getLanguageHints(),a=null;try{a=await f({type:e.type,length:this.opts.defaultLength,format:this.opts.format,preference:this.opts.preference,sharedContext:this.opts.sharedContext,outputLanguage:this.opts.outputLanguage,expectedInputLanguages:r,expectedContextLanguages:i})}catch(e){let t=e instanceof Error?e:Error(String(e));this.showError(`Failed to check model availability: ${t.message}`);return}if(!a||a===`unavailable`){this.showError(`Gemini Nano is not available on this device. See <a href="https://developer.chrome.com/docs/ai/summarizer-api#hardware_requirements" target="_blank" rel="noopener">hardware requirements ↗</a>.`);return}if((a===`downloadable`||a===`downloading`)&&this.opts.requireDownloadConsent&&!n){H(t.consentEl);return}await this.runSummarizer(e,t,a,n)}};function $(){let e=document.querySelector(`[data-ai-summarizer]`);if(!e)return;let t={},n=e.getAttribute(`data-ai-summarizer`);if(n&&n!==``&&n!==`true`)try{t=JSON.parse(n)}catch{}new Q(t).mount()}typeof document<`u`&&(document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,$):$()),exports.AISummarizer=Q;