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.
@@ -0,0 +1,408 @@
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.AISummarizer={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`ai-summarizer-styles`;function n(e){if(document.getElementById(t))return;let n=document.createElement(`style`);n.id=t,n.textContent=r(e),document.head.appendChild(n)}function r(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 i=`preferences`,a=`userPrefs`;function o(e){return new Promise((t,n)=>{let r=indexedDB.open(e,1);r.onupgradeneeded=e=>{e.target.result.createObjectStore(i)},r.onsuccess=e=>t(e.target.result),r.onerror=()=>n(r.error)})}async function s(e){try{let t=(await o(e)).transaction(i,`readonly`).objectStore(i).get(a);return await new Promise(e=>{t.onsuccess=()=>e(t.result??null),t.onerror=()=>e(null)})}catch{return null}}async function c(e,t){try{(await o(e)).transaction(i,`readwrite`).objectStore(i).put(t,a)}catch{}}var l=3.5,u=8e3;function d(){let e=globalThis.Summarizer;if(!e)throw Error(`Summarizer API is not available in this browser.`);return e}function f(){return`Summarizer`in globalThis}async function p(e={}){return d().availability({...e,expectedInputLanguages:x(e.expectedInputLanguages),expectedContextLanguages:x(e.expectedContextLanguages)})}var m=null,h=null;function g(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 _(e){let t=g(e);return m&&h===t?m:(m&&=((await m).destroy(),null),h=t,m=v(e),m)}async function v(e){return await p(e)!==`available`&&S(),d().create({type:e.type,length:e.length,format:e.format??`markdown`,preference:e.preference,sharedContext:e.sharedContext,outputLanguage:e.outputLanguage,expectedInputLanguages:x(e.expectedInputLanguages),expectedContextLanguages:x(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 y(e,t){let n=t?Math.floor(t*l):u;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 b(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 C(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 w(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=y(s,e),await c(s)}throw e}}function x(e){return e&&e.length>0?e:void 0}function S(){let e=globalThis.navigator;if(e?.userActivation&&!e.userActivation.isActive)throw Error(`Summarizer.create() must be called from a user gesture.`)}async function C(e,t,n,r){let i=t.inputQuota;if(!i)return y(e,void 0);if(t.measureInputUsage)try{if(await t.measureInputUsage(e,{context:n,signal:r})<=i)return e}catch{}return y(e,i)}async function*w(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 T(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 E(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>${D(T(e))}</span></li>`}else r&&=(n+=`</ul>`,!1),n+=`<p>${D(T(t))}</p>`}return r&&(n+=`</ul>`),n}function D(e){return e.replace(/\*\*(.*?)\*\*/g,`<strong>$1</strong>`).replace(/__(.*?)__/g,`<strong>$1</strong>`).replace(/\*(.*?)\*/g,`<em>$1</em>`).replace(/_(.*?)_/g,`<em>$1</em>`)}function O(e,t,n){let r=document.createElement(e);return r.className=t,n!==void 0&&(r.innerHTML=n),r}function k(e){for(let t of e.split(`,`).map(e=>e.trim())){let e=document.querySelector(t);if(e)return e}return null}function A(e){let t=(k(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 j(e,t){return t===`plain-text`?T(e).replace(/\n/g,`<br>`):E(e)}var M=`<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>`,N=`<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>`,P=`<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>`,F=`<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>`,I=`<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>`,L={"key-points":`Key Points`,tldr:`TL;DR`,teaser:`Teaser`,headline:`Headline`};function R(){let e=document.createElement(`div`);return e.className=`ais-root`,e}function z(e){let t=O(`div`,`ais-trigger`),n=O(`button`,`ais-btn`);return n.setAttribute(`aria-label`,`Summarize this article with on-device AI`),n.innerHTML=`${M} ${e}`,t.appendChild(n),{wrap:t,btn:n}}function B(e,t){e.disabled=!0,e.innerHTML=`<span class="ais-spinner" aria-hidden="true"></span> ${t}`}function V(e,t=`Regenerate`){e.disabled=!1,e.innerHTML=`${M} ${t}`}function H(e,t){let n=O(`div`,`ais-pills`);n.setAttribute(`role`,`group`),n.setAttribute(`aria-label`,`Summary style`);let r=O(`span`,`ais-pills__label`);r.textContent=`Style`,n.appendChild(r);let i=[];return Object.entries(L).forEach(([r,a])=>{let o=O(`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 U(e){e.classList.add(`ais-pills--visible`)}function W(e,t){let n=O(`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=O(`button`,`ais-btn`);i.innerHTML=`${I} Download &amp; summarize`,i.addEventListener(`click`,e);let a=O(`button`,`ais-btn ais-btn--ghost`);return a.textContent=`Not now`,a.addEventListener(`click`,t),r.appendChild(i),r.appendChild(a),n}function G(e){e.classList.add(`ais-consent--visible`)}function K(e){e.classList.remove(`ais-consent--visible`)}function q(){let e=O(`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 J(e){e.classList.remove(`ais-progress--visible`)}function Y(e,t,n){let r=O(`div`,`ais-card`);r.id=`ais-card`,r.setAttribute(`role`,`region`),r.setAttribute(`aria-label`,`AI-generated article summary`);let i=O(`div`,`ais-card__head`),a=O(`div`,`ais-card__title-row`),o=O(`h2`,`ais-card__title`);o.textContent=`AI Summary`;let s=O(`span`,`ais-card__badge`);s.textContent=`${L[e]} · ${t}`,a.appendChild(o),a.appendChild(s);let c=O(`div`,`ais-card__actions`),l=O(`button`,`ais-icon-btn`);l.setAttribute(`title`,`Copy summary`),l.setAttribute(`aria-label`,`Copy summary to clipboard`),l.innerHTML=N;let u=O(`button`,`ais-icon-btn ais-stop-btn`);u.setAttribute(`title`,`Stop generating`),u.setAttribute(`aria-label`,`Stop generating summary`),u.innerHTML=F;let d=O(`button`,`ais-icon-btn`);d.setAttribute(`title`,`Dismiss`),d.setAttribute(`aria-label`,`Dismiss summary`),d.innerHTML=P,c.appendChild(u),c.appendChild(l),c.appendChild(d),i.appendChild(a),i.appendChild(c);let f=O(`div`,`ais-card__body`),p=O(`div`,`ais-card__content`);p.innerHTML=`<span class="ais-cursor" aria-hidden="true"></span>`,f.appendChild(p);let m=O(`div`,`ais-card__foot`),h=O(`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=N,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=j(e,n),_(!1)},streamChunk:e=>{g=e,p.innerHTML=j(e,n),_(!1)},finalize:()=>{_(!1),u.hidden=!0}}}function X(e,t){let n=O(`div`,`ais-error`);n.id=`ais-error`,n.setAttribute(`role`,`alert`);let r=O(`span`,``);if(r.innerHTML=`⚠ ${e}`,n.appendChild(r),t){let e=O(`button`,`ais-error__retry`);e.textContent=`Retry`,e.addEventListener(`click`,()=>{n.remove(),t()}),n.appendChild(e)}return n}var Z=`.gh-content, .post-content, .article-body, article .content, .entry-content`,ee=`.gh-article-header, .post-header, .article-header, header.article`,te=`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.`,ne=new Set([`en`,`es`,`ja`]);function re(e){let t=e?.trim().toLowerCase();if(!t)return`en`;let n=t.split(`-`)[0];return ne.has(n)?n:`en`}var Q=class{opts;abortController=null;root=null;mounted=!1;isSummarizing=!1;constructor(e={}){this.opts={contentSelector:e.contentSelector??Z,anchorSelector:e.anchorSelector??ee,defaultType:e.defaultType??`key-points`,defaultLength:e.defaultLength??`medium`,format:e.format??`markdown`,preference:e.preference??`auto`,sharedContext:e.sharedContext??te,outputLanguage:re(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,!f()){this.opts.onUnsupported?.();return}n(this.opts.theme);let e={type:(await s(this.opts.dbName))?.type??this.opts.defaultType},t=k(this.opts.anchorSelector),r=k(this.opts.contentSelector)??document.querySelector(`article`)??document.querySelector(`main`);if(!r)return;this.root=R();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(),K(i.consentEl),await this.preflightAndRun(e,i,!1)}),t?t.insertAdjacentElement(`afterend`,this.root):r.insertAdjacentElement(`beforebegin`,this.root),this.warmup(e).catch(()=>{})}async warmup(e){if(!f())return;let{expectedInputLanguages:t,expectedContextLanguages:n}=this.getLanguageHints();try{await _({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}=z(this.opts.buttonLabel),{wrap:r,setProgress:i}=q(),a={},o=null,s=()=>{};if(this.opts.showTypeSwitcher){let{wrap:t,setDisabled:n}=H(e.type,async t=>{this.isSummarizing||(e.type=t,await c(this.opts.dbName,e),this.clearSummary(),await this.preflightAndRun(e,a,!1))});o=t,s=n}let l=W(async()=>{K(l),await this.preflightAndRun(e,a,!0)},()=>K(l));return a.triggerWrap=t,a.triggerBtn=n,a.pillsEl=o,a.setPillsDisabled=s,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=X(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:s,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),B(i,`Preparing…`),this.clearSummary(),K(l),J(o);let p=n===`downloadable`||n===`downloading`,m=null,h=null;try{p||(h=setTimeout(()=>{f.aborted||B(i,`Loading model…`)},1200)),m=await _({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=>{s(e),B(i,`Downloading… ${e.pct}%`)}:void 0}),h!==null&&(clearTimeout(h),h=null),J(o),B(i,`Summarizing…`);let n=A(this.opts.contentSelector);if(!n||n.length<80)throw Error(`Not enough article text found to summarize.`);let r=Y(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),V(i,this.opts.buttonLabel)});let l=document.title??``,g=await b({summarizer:m,text:n,context:`Article title: "${l}". Provide a clear, accurate summary.`,signal:f,onChunk:e=>r.streamChunk(e)});r.finalize(),a&&U(a),V(i,`Regenerate`),g&&this.opts.onSummary?.(g,e.type),await c(this.opts.dbName,e)}catch(n){if(n.name===`AbortError`){V(i,this.opts.buttonLabel);return}J(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)),V(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 p({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){G(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`,$):$()),e.AISummarizer=Q});
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "browser-ai-summarizer",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc -p tsconfig.build.json && vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "main": "dist/browser-ai-summarizer.cjs.cjs",
11
+ "module": "dist/browser-ai-summarizer.esm.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/browser-ai-summarizer.esm.js",
16
+ "require": "./dist/browser-ai-summarizer.cjs.cjs",
17
+ "types": "./dist/index.d.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "devDependencies": {
24
+ "typescript": "~5.9.3",
25
+ "vite": "^8.0.0"
26
+ }
27
+ }