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,961 @@
1
+ //#region src/core/styles.ts
2
+ var e = "ai-summarizer-styles";
3
+ function t(t) {
4
+ if (document.getElementById(e)) return;
5
+ let r = document.createElement("style");
6
+ r.id = e, r.textContent = n(t), document.head.appendChild(r);
7
+ }
8
+ function n(e) {
9
+ let t = {
10
+ buttonBg: e.buttonBg ?? "#111111",
11
+ buttonColor: e.buttonColor ?? "#f7f3ed",
12
+ buttonRadius: e.buttonRadius ?? "3px",
13
+ buttonFont: e.buttonFont ?? "system-ui, -apple-system, sans-serif",
14
+ accentColor: e.accentColor ?? "#111111",
15
+ cardBg: e.cardBg ?? "#faf8f5",
16
+ cardHeaderBg: e.cardHeaderBg ?? "#f3ede5",
17
+ cardBorder: e.cardBorder ?? "#e4ddd4",
18
+ cardFont: e.cardFont ?? "system-ui, -apple-system, sans-serif",
19
+ zIndex: e.zIndex ?? 1
20
+ };
21
+ return `
22
+ /* ── AI Summarizer Widget ────────────────────────────────── */
23
+ .ais-root {
24
+ --ais-btn-bg: ${t.buttonBg};
25
+ --ais-btn-color: ${t.buttonColor};
26
+ --ais-btn-radius: ${t.buttonRadius};
27
+ --ais-btn-font: ${t.buttonFont};
28
+ --ais-accent: ${t.accentColor};
29
+ --ais-card-bg: ${t.cardBg};
30
+ --ais-card-head-bg: ${t.cardHeaderBg};
31
+ --ais-card-border: ${t.cardBorder};
32
+ --ais-card-font: ${t.cardFont};
33
+ --ais-z: ${t.zIndex};
34
+ }
35
+
36
+ /* ── Trigger ── */
37
+ .ais-trigger {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 0.75rem;
41
+ margin: 1.75rem 0 0;
42
+ flex-wrap: wrap;
43
+ position: relative;
44
+ z-index: var(--ais-z);
45
+ }
46
+
47
+ .ais-btn {
48
+ display: inline-flex;
49
+ align-items: center;
50
+ gap: 0.45rem;
51
+ padding: 0.55rem 1.15rem;
52
+ background: var(--ais-btn-bg);
53
+ color: var(--ais-btn-color);
54
+ font-family: var(--ais-btn-font);
55
+ font-size: 0.8rem;
56
+ font-weight: 500;
57
+ letter-spacing: 0.05em;
58
+ text-transform: uppercase;
59
+ border: 1.5px solid var(--ais-btn-bg);
60
+ cursor: pointer;
61
+ border-radius: var(--ais-btn-radius);
62
+ line-height: 1;
63
+ transition: filter 0.15s, opacity 0.2s, transform 0.1s;
64
+ user-select: none;
65
+ }
66
+ .ais-btn:hover:not(:disabled) { filter: brightness(1.15); }
67
+ .ais-btn:active:not(:disabled) { transform: scale(0.97); }
68
+ .ais-btn:disabled { opacity: 0.4; cursor: not-allowed; }
69
+ .ais-btn svg { width: 14px; height: 14px; display: block; }
70
+
71
+ .ais-btn--ghost {
72
+ background: transparent;
73
+ color: #666;
74
+ border-color: #d5cfc7;
75
+ }
76
+ .ais-btn--ghost:hover:not(:disabled) { filter: none; background: #f5f5f5; }
77
+
78
+ /* ── Spinner ── */
79
+ .ais-spinner {
80
+ display: inline-block;
81
+ width: 1em;
82
+ height: 1em;
83
+ border: 0.15em solid rgba(255,255,255,0.2);
84
+ border-top-color: currentColor;
85
+ border-radius: 50%;
86
+ animation: ais-spin 0.65s linear infinite;
87
+ flex-shrink: 0;
88
+ vertical-align: -0.125em;
89
+ box-sizing: border-box;
90
+ }
91
+ .ais-spinner--dark {
92
+ border-color: rgba(0,0,0,0.12);
93
+ border-top-color: #333;
94
+ }
95
+ @keyframes ais-spin { to { transform: rotate(360deg); } }
96
+
97
+ /* ── Type pills ── */
98
+ .ais-pills {
99
+ display: none;
100
+ align-items: center;
101
+ gap: 0.35rem;
102
+ flex-wrap: wrap;
103
+ margin: 0.75rem 0 0;
104
+ }
105
+ .ais-pills--visible { display: flex; }
106
+ .ais-pills__label {
107
+ font-family: var(--ais-btn-font);
108
+ font-size: 0.7rem;
109
+ color: #999;
110
+ letter-spacing: 0.06em;
111
+ text-transform: uppercase;
112
+ margin-right: 0.15rem;
113
+ }
114
+ .ais-pill {
115
+ font-family: var(--ais-btn-font);
116
+ font-size: 0.7rem;
117
+ font-weight: 500;
118
+ letter-spacing: 0.05em;
119
+ text-transform: uppercase;
120
+ padding: 0.22rem 0.6rem;
121
+ border: 1px solid #ddd;
122
+ border-radius: 100px;
123
+ background: #fff;
124
+ color: #777;
125
+ cursor: pointer;
126
+ transition: all 0.13s;
127
+ line-height: 1;
128
+ }
129
+ .ais-pill:hover:not(:disabled):not(.ais-pill--active) { border-color: #999; color: #333; }
130
+ .ais-pill:disabled {
131
+ opacity: 0.5;
132
+ cursor: not-allowed;
133
+ filter: grayscale(0.5);
134
+ }
135
+ .ais-pill--active {
136
+ background: var(--ais-accent);
137
+ border-color: var(--ais-accent);
138
+ color: #fff;
139
+ }
140
+
141
+ /* ── Consent dialog ── */
142
+ .ais-consent {
143
+ display: none;
144
+ margin: 1.1rem 0 0;
145
+ padding: 1.1rem 1.4rem;
146
+ background: #fffdf9;
147
+ border: 1px solid #e8e3db;
148
+ border-left: 3px solid #c8a96a;
149
+ border-radius: 0 4px 4px 0;
150
+ font-family: var(--ais-card-font);
151
+ animation: ais-fade-up 0.25s ease;
152
+ }
153
+ .ais-consent--visible { display: block; }
154
+ .ais-consent__title {
155
+ font-size: 0.82rem;
156
+ font-weight: 600;
157
+ color: #111;
158
+ margin: 0 0 0.4rem;
159
+ }
160
+ .ais-consent__body {
161
+ font-size: 0.82rem;
162
+ color: #555;
163
+ line-height: 1.65;
164
+ margin: 0 0 0.9rem;
165
+ }
166
+ .ais-consent__body strong { color: #333; font-weight: 600; }
167
+ .ais-consent__actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
168
+
169
+ /* ── Download progress ── */
170
+ .ais-progress {
171
+ display: none;
172
+ margin: 1.1rem 0 0;
173
+ padding: 0.9rem 1.1rem;
174
+ background: var(--ais-card-bg);
175
+ border: 1px solid var(--ais-card-border);
176
+ border-radius: 4px;
177
+ font-family: var(--ais-card-font);
178
+ }
179
+ .ais-progress--visible { display: block; }
180
+ .ais-progress__top {
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: space-between;
184
+ margin-bottom: 0.5rem;
185
+ gap: 0.5rem;
186
+ }
187
+ .ais-progress__label {
188
+ display: flex;
189
+ align-items: center;
190
+ gap: 0.45rem;
191
+ font-size: 0.8rem;
192
+ color: #555;
193
+ }
194
+ .ais-progress__pct {
195
+ font-size: 0.78rem;
196
+ font-weight: 600;
197
+ color: #111;
198
+ font-variant-numeric: tabular-nums;
199
+ }
200
+ .ais-progress__bar-bg {
201
+ height: 3px;
202
+ background: #e0dbd3;
203
+ border-radius: 2px;
204
+ overflow: hidden;
205
+ }
206
+ .ais-progress__bar {
207
+ height: 100%;
208
+ background: var(--ais-accent);
209
+ border-radius: 2px;
210
+ width: 0%;
211
+ transition: width 0.25s ease;
212
+ }
213
+ .ais-progress__size {
214
+ margin-top: 0.45rem;
215
+ font-size: 0.72rem;
216
+ color: #666;
217
+ font-variant-numeric: tabular-nums;
218
+ }
219
+ .ais-progress__note { margin-top: 0.35rem; font-size: 0.72rem; color: #aaa; }
220
+
221
+ /* ── Summary card ── */
222
+ .ais-card {
223
+ margin: 1.5rem 0 0;
224
+ border: 1px solid var(--ais-card-border);
225
+ border-left: 3px solid var(--ais-accent);
226
+ background: var(--ais-card-bg);
227
+ border-radius: 0 4px 4px 0;
228
+ overflow: hidden;
229
+ font-family: var(--ais-card-font);
230
+ animation: ais-fade-up 0.35s ease forwards;
231
+ }
232
+ @keyframes ais-fade-up {
233
+ from { opacity: 0; transform: translateY(5px); }
234
+ to { opacity: 1; transform: translateY(0); }
235
+ }
236
+
237
+ .ais-card__head {
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: space-between;
241
+ padding: 0.8rem 1.2rem;
242
+ border-bottom: 1px solid var(--ais-card-border);
243
+ background: var(--ais-card-head-bg);
244
+ gap: 0.5rem;
245
+ flex-wrap: wrap;
246
+ }
247
+ .ais-card__title-row { display: flex; align-items: baseline; gap: 0.55rem; }
248
+ .ais-card__title {
249
+ font-size: 0.9rem;
250
+ font-weight: 600;
251
+ color: #111;
252
+ margin: 0;
253
+ font-family: var(--ais-card-font);
254
+ }
255
+ .ais-card__badge {
256
+ font-size: 0.63rem;
257
+ font-weight: 500;
258
+ letter-spacing: 0.08em;
259
+ text-transform: uppercase;
260
+ color: #888;
261
+ border: 1px solid #d5cfc7;
262
+ padding: 0.1rem 0.38rem;
263
+ border-radius: 2px;
264
+ background: #fff;
265
+ white-space: nowrap;
266
+ }
267
+ .ais-card__actions { display: flex; gap: 0.25rem; align-items: center; }
268
+ .ais-icon-btn {
269
+ background: none;
270
+ border: none;
271
+ cursor: pointer;
272
+ padding: 0.28rem;
273
+ color: #aaa;
274
+ border-radius: 3px;
275
+ line-height: 0;
276
+ transition: color 0.13s, background 0.13s;
277
+ }
278
+ .ais-icon-btn:hover { color: #333; background: rgba(0,0,0,0.06); }
279
+ .ais-icon-btn svg { width: 14px; height: 14px; display: block; }
280
+
281
+ .ais-card__body { padding: 1.1rem 1.4rem 0.9rem; }
282
+ .ais-card__content {
283
+ font-size: 0.89rem;
284
+ color: #2a2a2a;
285
+ line-height: 1.78;
286
+ min-height: 1.5rem;
287
+ }
288
+ .ais-card__content ul {
289
+ list-style: none;
290
+ padding: 0; margin: 0;
291
+ display: flex;
292
+ flex-direction: column;
293
+ gap: 0.4rem;
294
+ }
295
+ .ais-card__content li {
296
+ display: flex;
297
+ align-items: flex-start;
298
+ gap: 0.6rem;
299
+ padding: 0.5rem 0.75rem;
300
+ background: #fff;
301
+ border: 1px solid #ece7df;
302
+ border-radius: 3px;
303
+ font-size: 0.875rem;
304
+ line-height: 1.65;
305
+ color: #333;
306
+ }
307
+ .ais-card__content li::before {
308
+ content: '›';
309
+ color: #999;
310
+ font-size: 1.05rem;
311
+ line-height: 1.45;
312
+ flex-shrink: 0;
313
+ }
314
+ .ais-card__content li span {
315
+ flex: 1;
316
+ }
317
+ .ais-card__content p { margin: 0 0 0.6rem; }
318
+ .ais-card__content p:last-child { margin: 0; }
319
+ .ais-card__content strong { font-weight: 600; color: #111; }
320
+
321
+ /* Streaming cursor */
322
+ .ais-cursor {
323
+ display: inline-block;
324
+ width: 2px; height: 0.85em;
325
+ background: #666;
326
+ vertical-align: text-bottom;
327
+ margin-left: 2px;
328
+ animation: ais-blink 0.85s step-end infinite;
329
+ }
330
+ @keyframes ais-blink { 50% { opacity: 0; } }
331
+
332
+ .ais-card__foot {
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: space-between;
336
+ padding: 0.55rem 1.4rem;
337
+ border-top: 1px solid #ece7df;
338
+ gap: 0.5rem;
339
+ flex-wrap: wrap;
340
+ }
341
+ .ais-card__note {
342
+ font-size: 0.68rem;
343
+ color: #bbb;
344
+ letter-spacing: 0.03em;
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 0.35rem;
348
+ }
349
+ .ais-card__note-dot {
350
+ width: 3px; height: 3px;
351
+ background: #ccc; border-radius: 50%; flex-shrink: 0;
352
+ }
353
+
354
+ /* ── Error ── */
355
+ .ais-error {
356
+ margin: 1.1rem 0 0;
357
+ padding: 0.9rem 1.1rem;
358
+ background: #fff8f7;
359
+ border: 1px solid #f0cac7;
360
+ border-left: 3px solid #b94040;
361
+ border-radius: 0 4px 4px 0;
362
+ font-family: var(--ais-card-font);
363
+ font-size: 0.82rem;
364
+ color: #7a2a2a;
365
+ display: flex;
366
+ align-items: flex-start;
367
+ justify-content: space-between;
368
+ gap: 0.75rem;
369
+ animation: ais-fade-up 0.25s ease;
370
+ }
371
+ .ais-error a { color: inherit; }
372
+ .ais-error__retry {
373
+ font-family: var(--ais-btn-font);
374
+ font-size: 0.76rem;
375
+ font-weight: 500;
376
+ color: #7a2a2a;
377
+ background: none;
378
+ border: 1px solid #f0cac7;
379
+ border-radius: 3px;
380
+ padding: 0.28rem 0.65rem;
381
+ cursor: pointer;
382
+ white-space: nowrap;
383
+ flex-shrink: 0;
384
+ transition: background 0.13s;
385
+ }
386
+ .ais-error__retry:hover { background: rgba(0,0,0,0.04); }
387
+ `;
388
+ }
389
+ //#endregion
390
+ //#region src/core/prefs.ts
391
+ var r = "preferences", i = "userPrefs";
392
+ function a(e) {
393
+ return new Promise((t, n) => {
394
+ let i = indexedDB.open(e, 1);
395
+ i.onupgradeneeded = (e) => {
396
+ e.target.result.createObjectStore(r);
397
+ }, i.onsuccess = (e) => t(e.target.result), i.onerror = () => n(i.error);
398
+ });
399
+ }
400
+ async function o(e) {
401
+ try {
402
+ let t = (await a(e)).transaction(r, "readonly").objectStore(r).get(i);
403
+ return await new Promise((e) => {
404
+ t.onsuccess = () => e(t.result ?? null), t.onerror = () => e(null);
405
+ });
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+ async function s(e, t) {
411
+ try {
412
+ (await a(e)).transaction(r, "readwrite").objectStore(r).put(t, i);
413
+ } catch {}
414
+ }
415
+ //#endregion
416
+ //#region src/core/summarizer.ts
417
+ var c = 3.5, l = 8e3;
418
+ function u() {
419
+ let e = globalThis.Summarizer;
420
+ if (!e) throw Error("Summarizer API is not available in this browser.");
421
+ return e;
422
+ }
423
+ function d() {
424
+ return "Summarizer" in globalThis;
425
+ }
426
+ async function f(e = {}) {
427
+ return u().availability({
428
+ ...e,
429
+ expectedInputLanguages: b(e.expectedInputLanguages),
430
+ expectedContextLanguages: b(e.expectedContextLanguages)
431
+ });
432
+ }
433
+ var p = null, m = null;
434
+ function h(e) {
435
+ return JSON.stringify({
436
+ type: e.type,
437
+ length: e.length,
438
+ format: e.format,
439
+ preference: e.preference,
440
+ sharedContext: e.sharedContext,
441
+ outputLanguage: e.outputLanguage,
442
+ expectedInputLanguages: e.expectedInputLanguages,
443
+ expectedContextLanguages: e.expectedContextLanguages
444
+ });
445
+ }
446
+ async function g(e) {
447
+ let t = h(e);
448
+ return p && m === t ? p : (p &&= ((await p).destroy(), null), m = t, p = _(e), p);
449
+ }
450
+ async function _(e) {
451
+ return await f(e) !== "available" && x(), u().create({
452
+ type: e.type,
453
+ length: e.length,
454
+ format: e.format ?? "markdown",
455
+ preference: e.preference,
456
+ sharedContext: e.sharedContext,
457
+ outputLanguage: e.outputLanguage,
458
+ expectedInputLanguages: b(e.expectedInputLanguages),
459
+ expectedContextLanguages: b(e.expectedContextLanguages),
460
+ signal: e.signal,
461
+ monitor: e.onDownloadProgress ? (t) => {
462
+ t.addEventListener("downloadprogress", (t) => {
463
+ 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;
464
+ e.onDownloadProgress?.({
465
+ pct: o,
466
+ loaded: r,
467
+ total: i,
468
+ lengthComputable: a
469
+ });
470
+ });
471
+ } : void 0
472
+ });
473
+ }
474
+ function v(e, t) {
475
+ let n = t ? Math.floor(t * c) : l;
476
+ if (e.length <= n) return e;
477
+ let r = e.lastIndexOf("\n\n", n), i = r > n * .6 ? r : n;
478
+ return e.slice(0, i) + "\n\n[… content truncated for summary]";
479
+ }
480
+ async function y(e) {
481
+ let { summarizer: t, context: n, signal: r, onChunk: i } = e, a = new TextDecoder(), o = (e) => {
482
+ if (typeof e == "string") return e;
483
+ if (e instanceof Uint8Array) return a.decode(e);
484
+ if (e instanceof ArrayBuffer) return a.decode(new Uint8Array(e));
485
+ if (e && typeof e == "object") {
486
+ let t = e.text;
487
+ if (typeof t == "string") return t;
488
+ let n = e.output;
489
+ if (typeof n == "string") return n;
490
+ let r = e.outputText;
491
+ if (typeof r == "string") return r;
492
+ }
493
+ return null;
494
+ }, s = await S(e.text, t, n, r), c = async (e) => {
495
+ let a = t.summarizeStreaming(e, {
496
+ context: n,
497
+ signal: r
498
+ }), s = "", c = !1;
499
+ for await (let e of C(a)) {
500
+ if (r.aborted) break;
501
+ let t = o(e);
502
+ t && (t.startsWith(s) ? s = t : s += t, c = !0, i(s));
503
+ }
504
+ if (!c && t.summarize) {
505
+ let a = await t.summarize(e, {
506
+ context: n,
507
+ signal: r
508
+ });
509
+ return a && i(a), a;
510
+ }
511
+ return s;
512
+ };
513
+ try {
514
+ return await c(s);
515
+ } catch (e) {
516
+ if (e.name === "QuotaExceededError") {
517
+ let e = t.inputQuota ? Math.floor(t.inputQuota * .7) : void 0;
518
+ return s = v(s, e), await c(s);
519
+ }
520
+ throw e;
521
+ }
522
+ }
523
+ function b(e) {
524
+ return e && e.length > 0 ? e : void 0;
525
+ }
526
+ function x() {
527
+ let e = globalThis.navigator;
528
+ if (e?.userActivation && !e.userActivation.isActive) throw Error("Summarizer.create() must be called from a user gesture.");
529
+ }
530
+ async function S(e, t, n, r) {
531
+ let i = t.inputQuota;
532
+ if (!i) return v(e, void 0);
533
+ if (t.measureInputUsage) try {
534
+ if (await t.measureInputUsage(e, {
535
+ context: n,
536
+ signal: r
537
+ }) <= i) return e;
538
+ } catch {}
539
+ return v(e, i);
540
+ }
541
+ async function* C(e) {
542
+ if (e && typeof e[Symbol.asyncIterator] == "function") {
543
+ yield* e;
544
+ return;
545
+ }
546
+ if (e && typeof e.getReader == "function") {
547
+ let t = e.getReader();
548
+ try {
549
+ for (;;) {
550
+ let { value: e, done: n } = await t.read();
551
+ if (n) break;
552
+ yield e;
553
+ }
554
+ } finally {
555
+ t.releaseLock();
556
+ }
557
+ return;
558
+ }
559
+ throw Error("Unsupported summary stream type.");
560
+ }
561
+ //#endregion
562
+ //#region src/utils/markdown.ts
563
+ function w(e) {
564
+ return e.replace(/[&<>"']/g, (e) => {
565
+ switch (e) {
566
+ case "&": return "&amp;";
567
+ case "<": return "&lt;";
568
+ case ">": return "&gt;";
569
+ case "\"": return "&quot;";
570
+ case "'": return "&#39;";
571
+ default: return e;
572
+ }
573
+ });
574
+ }
575
+ function T(e) {
576
+ let t = e.trim().split("\n"), n = "", r = !1;
577
+ for (let e of t) {
578
+ let t = e.trim();
579
+ if (!t) {
580
+ r &&= (n += "</ul>", !1);
581
+ continue;
582
+ }
583
+ let i = /^[-*•]\s+/.test(t), a = /^\d+\.\s+/.test(t);
584
+ if (i || a) {
585
+ r ||= (n += "<ul>", !0);
586
+ let e = t.replace(/^([-*•]|\d+\.)\s+/, "");
587
+ n += `<li><span>${E(w(e))}</span></li>`;
588
+ } else r &&= (n += "</ul>", !1), n += `<p>${E(w(t))}</p>`;
589
+ }
590
+ return r && (n += "</ul>"), n;
591
+ }
592
+ function E(e) {
593
+ return e.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>").replace(/__(.*?)__/g, "<strong>$1</strong>").replace(/\*(.*?)\*/g, "<em>$1</em>").replace(/_(.*?)_/g, "<em>$1</em>");
594
+ }
595
+ //#endregion
596
+ //#region src/utils/dom.ts
597
+ function D(e, t, n) {
598
+ let r = document.createElement(e);
599
+ return r.className = t, n !== void 0 && (r.innerHTML = n), r;
600
+ }
601
+ function O(e) {
602
+ for (let t of e.split(",").map((e) => e.trim())) {
603
+ let e = document.querySelector(t);
604
+ if (e) return e;
605
+ }
606
+ return null;
607
+ }
608
+ function k(e) {
609
+ let t = (O(e) ?? document.querySelector("article") ?? document.querySelector("main") ?? document.body).cloneNode(!0);
610
+ 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, "\n\n").trim();
611
+ }
612
+ function A(e, t) {
613
+ return t === "plain-text" ? w(e).replace(/\n/g, "<br>") : T(e);
614
+ }
615
+ //#endregion
616
+ //#region src/core/ui.ts
617
+ 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;\">\n <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\"/>\n <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\"/>\n</svg>", M = "<svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect x=\"5\" y=\"5\" width=\"9\" height=\"9\" rx=\"1.5\" stroke=\"currentColor\" stroke-width=\"1.3\"/>\n <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\"\n stroke=\"currentColor\" stroke-width=\"1.3\"/>\n</svg>", N = "<svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M3 3l10 10M13 3L3 13\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n</svg>", ee = "<svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect x=\"3\" y=\"3\" width=\"10\" height=\"10\" rx=\"1.5\" fill=\"currentColor\"/>\n</svg>", te = "<svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <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\"/>\n</svg>", P = {
618
+ "key-points": "Key Points",
619
+ tldr: "TL;DR",
620
+ teaser: "Teaser",
621
+ headline: "Headline"
622
+ };
623
+ function F() {
624
+ let e = document.createElement("div");
625
+ return e.className = "ais-root", e;
626
+ }
627
+ function I(e) {
628
+ let t = D("div", "ais-trigger"), n = D("button", "ais-btn");
629
+ return n.setAttribute("aria-label", "Summarize this article with on-device AI"), n.innerHTML = `${j} ${e}`, t.appendChild(n), {
630
+ wrap: t,
631
+ btn: n
632
+ };
633
+ }
634
+ function L(e, t) {
635
+ e.disabled = !0, e.innerHTML = `<span class="ais-spinner" aria-hidden="true"></span> ${t}`;
636
+ }
637
+ function R(e, t = "Regenerate") {
638
+ e.disabled = !1, e.innerHTML = `${j} ${t}`;
639
+ }
640
+ function z(e, t) {
641
+ let n = D("div", "ais-pills");
642
+ n.setAttribute("role", "group"), n.setAttribute("aria-label", "Summary style");
643
+ let r = D("span", "ais-pills__label");
644
+ r.textContent = "Style", n.appendChild(r);
645
+ let i = [];
646
+ return Object.entries(P).forEach(([r, a]) => {
647
+ let o = D("button", `ais-pill${r === e ? " ais-pill--active" : ""}`);
648
+ o.textContent = a, o.dataset.type = r, o.addEventListener("click", () => {
649
+ 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));
650
+ }), n.appendChild(o), i.push(o);
651
+ }), {
652
+ wrap: n,
653
+ setDisabled: (e) => {
654
+ i.forEach((t) => {
655
+ t.disabled = e, t.setAttribute("aria-disabled", String(e));
656
+ });
657
+ }
658
+ };
659
+ }
660
+ function B(e) {
661
+ e.classList.add("ais-pills--visible");
662
+ }
663
+ function V(e, t) {
664
+ let n = D("div", "ais-consent");
665
+ n.setAttribute("role", "dialog"), n.setAttribute("aria-modal", "false"), n.setAttribute("aria-labelledby", "ais-consent-title"), n.innerHTML = "\n <div class=\"ais-consent__title\" id=\"ais-consent-title\">⬇ Download AI model to continue</div>\n <div class=\"ais-consent__body\">\n Summaries run <strong>entirely on your device</strong> using Gemini Nano — no content\n leaves your browser. Browser needs to download this model once\n (<strong>~1–2 GB</strong>), then it's cached for all future use.\n </div>\n <div class=\"ais-consent__actions\"></div>\n ";
666
+ let r = n.querySelector(".ais-consent__actions"), i = D("button", "ais-btn");
667
+ i.innerHTML = `${te} Download &amp; summarize`, i.addEventListener("click", e);
668
+ let a = D("button", "ais-btn ais-btn--ghost");
669
+ return a.textContent = "Not now", a.addEventListener("click", t), r.appendChild(i), r.appendChild(a), n;
670
+ }
671
+ function H(e) {
672
+ e.classList.add("ais-consent--visible");
673
+ }
674
+ function U(e) {
675
+ e.classList.remove("ais-consent--visible");
676
+ }
677
+ function W() {
678
+ let e = D("div", "ais-progress");
679
+ e.setAttribute("role", "status"), e.setAttribute("aria-live", "polite"), e.innerHTML = "\n <div class=\"ais-progress__top\">\n <div class=\"ais-progress__label\">\n <span class=\"ais-spinner ais-spinner--dark\" aria-hidden=\"true\"></span>\n Downloading Gemini Nano…\n </div>\n <span class=\"ais-progress__pct\">0%</span>\n </div>\n <div class=\"ais-progress__bar-bg\">\n <div class=\"ais-progress__bar\"></div>\n </div>\n <div class=\"ais-progress__size\">Exact size hidden by the browser</div>\n <div class=\"ais-progress__note\">One-time download · cached in Browser for future use</div>\n ";
680
+ let t = e.querySelector(".ais-progress__bar"), n = e.querySelector(".ais-progress__pct"), r = e.querySelector(".ais-progress__size"), i = (e) => {
681
+ if (!Number.isFinite(e) || e <= 0) return "0 B";
682
+ let t = [
683
+ "B",
684
+ "KB",
685
+ "MB",
686
+ "GB",
687
+ "TB"
688
+ ], n = e, r = 0;
689
+ for (; n >= 1024 && r < t.length - 1;) n /= 1024, r += 1;
690
+ let i = n >= 100 ? 0 : n >= 10 ? 1 : 2;
691
+ return `${n.toFixed(i)} ${t[r]}`;
692
+ };
693
+ return {
694
+ wrap: e,
695
+ setProgress: (a) => {
696
+ let o = Math.max(0, Math.min(100, Math.round(a.pct)));
697
+ 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");
698
+ }
699
+ };
700
+ }
701
+ function G(e) {
702
+ e.classList.remove("ais-progress--visible");
703
+ }
704
+ function K(e, t, n) {
705
+ let r = D("div", "ais-card");
706
+ r.id = "ais-card", r.setAttribute("role", "region"), r.setAttribute("aria-label", "AI-generated article summary");
707
+ let i = D("div", "ais-card__head"), a = D("div", "ais-card__title-row"), o = D("h2", "ais-card__title");
708
+ o.textContent = "AI Summary";
709
+ let s = D("span", "ais-card__badge");
710
+ s.textContent = `${P[e]} · ${t}`, a.appendChild(o), a.appendChild(s);
711
+ let c = D("div", "ais-card__actions"), l = D("button", "ais-icon-btn");
712
+ l.setAttribute("title", "Copy summary"), l.setAttribute("aria-label", "Copy summary to clipboard"), l.innerHTML = M;
713
+ let u = D("button", "ais-icon-btn ais-stop-btn");
714
+ u.setAttribute("title", "Stop generating"), u.setAttribute("aria-label", "Stop generating summary"), u.innerHTML = ee;
715
+ let d = D("button", "ais-icon-btn");
716
+ 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);
717
+ let f = D("div", "ais-card__body"), p = D("div", "ais-card__content");
718
+ p.innerHTML = "<span class=\"ais-cursor\" aria-hidden=\"true\"></span>", f.appendChild(p);
719
+ let m = D("div", "ais-card__foot"), h = D("span", "ais-card__note");
720
+ 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);
721
+ let g = "";
722
+ l.addEventListener("click", async () => {
723
+ if (g) try {
724
+ await navigator.clipboard.writeText(g), l.innerHTML = "✓", l.style.color = "#4a9a5c", setTimeout(() => {
725
+ l.innerHTML = M, l.style.color = "";
726
+ }, 2e3);
727
+ } catch {}
728
+ }), d.addEventListener("click", () => r.remove());
729
+ let _ = (e) => {
730
+ let t = p.querySelector(".ais-cursor");
731
+ e && !t && p.insertAdjacentHTML("beforeend", "<span class=\"ais-cursor\" aria-hidden=\"true\"></span>"), e || t?.remove();
732
+ };
733
+ return {
734
+ card: r,
735
+ contentEl: p,
736
+ stopBtn: u,
737
+ setCursor: _,
738
+ setContent: (e) => {
739
+ g = e, p.innerHTML = A(e, n), _(!1);
740
+ },
741
+ streamChunk: (e) => {
742
+ g = e, p.innerHTML = A(e, n), _(!1);
743
+ },
744
+ finalize: () => {
745
+ _(!1), u.hidden = !0;
746
+ }
747
+ };
748
+ }
749
+ function q(e, t) {
750
+ let n = D("div", "ais-error");
751
+ n.id = "ais-error", n.setAttribute("role", "alert");
752
+ let r = D("span", "");
753
+ if (r.innerHTML = `⚠ ${e}`, n.appendChild(r), t) {
754
+ let e = D("button", "ais-error__retry");
755
+ e.textContent = "Retry", e.addEventListener("click", () => {
756
+ n.remove(), t();
757
+ }), n.appendChild(e);
758
+ }
759
+ return n;
760
+ }
761
+ //#endregion
762
+ //#region src/index.ts
763
+ 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([
764
+ "en",
765
+ "es",
766
+ "ja"
767
+ ]);
768
+ function ne(e) {
769
+ let t = e?.trim().toLowerCase();
770
+ if (!t) return "en";
771
+ let n = t.split("-")[0];
772
+ return Z.has(n) ? n : "en";
773
+ }
774
+ var Q = class {
775
+ opts;
776
+ abortController = null;
777
+ root = null;
778
+ mounted = !1;
779
+ isSummarizing = !1;
780
+ constructor(e = {}) {
781
+ this.opts = {
782
+ contentSelector: e.contentSelector ?? J,
783
+ anchorSelector: e.anchorSelector ?? Y,
784
+ defaultType: e.defaultType ?? "key-points",
785
+ defaultLength: e.defaultLength ?? "medium",
786
+ format: e.format ?? "markdown",
787
+ preference: e.preference ?? "auto",
788
+ sharedContext: e.sharedContext ?? X,
789
+ outputLanguage: ne(e.outputLanguage),
790
+ expectedInputLanguages: e.expectedInputLanguages ?? [],
791
+ expectedContextLanguages: e.expectedContextLanguages ?? [],
792
+ buttonLabel: e.buttonLabel ?? "Summarize with AI",
793
+ showTypeSwitcher: e.showTypeSwitcher ?? !0,
794
+ requireDownloadConsent: e.requireDownloadConsent ?? !0,
795
+ dbName: e.dbName ?? "ai-summarizer",
796
+ theme: e.theme ?? {},
797
+ onSummary: e.onSummary,
798
+ onError: e.onError,
799
+ onUnsupported: e.onUnsupported
800
+ };
801
+ }
802
+ async mount() {
803
+ if (this.mounted) return;
804
+ if (this.mounted = !0, !d()) {
805
+ this.opts.onUnsupported?.();
806
+ return;
807
+ }
808
+ t(this.opts.theme);
809
+ 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");
810
+ if (!r) return;
811
+ this.root = F();
812
+ let i = this.buildUI(e);
813
+ 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 () => {
814
+ this.clearSummary(), U(i.consentEl), await this.preflightAndRun(e, i, !1);
815
+ }), n ? n.insertAdjacentElement("afterend", this.root) : r.insertAdjacentElement("beforebegin", this.root), this.warmup(e).catch(() => {});
816
+ }
817
+ async warmup(e) {
818
+ if (!d()) return;
819
+ let { expectedInputLanguages: t, expectedContextLanguages: n } = this.getLanguageHints();
820
+ try {
821
+ await g({
822
+ type: e?.type ?? this.opts.defaultType,
823
+ length: this.opts.defaultLength,
824
+ format: this.opts.format,
825
+ preference: this.opts.preference,
826
+ sharedContext: this.opts.sharedContext,
827
+ outputLanguage: this.opts.outputLanguage,
828
+ expectedInputLanguages: t,
829
+ expectedContextLanguages: n
830
+ });
831
+ } catch {}
832
+ }
833
+ buildUI(e) {
834
+ let { wrap: t, btn: n } = I(this.opts.buttonLabel), { wrap: r, setProgress: i } = W(), a = {}, o = null, c = () => {};
835
+ if (this.opts.showTypeSwitcher) {
836
+ let { wrap: t, setDisabled: n } = z(e.type, async (t) => {
837
+ this.isSummarizing || (e.type = t, await s(this.opts.dbName, e), this.clearSummary(), await this.preflightAndRun(e, a, !1));
838
+ });
839
+ o = t, c = n;
840
+ }
841
+ let l = V(async () => {
842
+ U(l), await this.preflightAndRun(e, a, !0);
843
+ }, () => U(l));
844
+ return a.triggerWrap = t, a.triggerBtn = n, a.pillsEl = o, a.setPillsDisabled = c, a.progressEl = r, a.progressSetProgress = i, a.consentEl = l, a;
845
+ }
846
+ setSummarizingState(e, t) {
847
+ this.isSummarizing = t, e.setPillsDisabled(t);
848
+ }
849
+ async summarize() {
850
+ this.root || await this.mount(), this.root?.querySelector(".ais-btn")?.click();
851
+ }
852
+ destroy() {
853
+ this.abortController?.abort(), this.root?.remove(), this.root = null, this.mounted = !1;
854
+ }
855
+ clearSummary() {
856
+ this.root?.querySelector("#ais-card")?.remove(), this.root?.querySelector("#ais-error")?.remove();
857
+ }
858
+ showError(e, t) {
859
+ this.clearSummary();
860
+ let n = q(e, t);
861
+ this.root?.appendChild(n);
862
+ }
863
+ getLanguageHints() {
864
+ return {
865
+ expectedInputLanguages: this.opts.expectedInputLanguages?.length ? this.opts.expectedInputLanguages : void 0,
866
+ expectedContextLanguages: this.opts.expectedContextLanguages?.length ? this.opts.expectedContextLanguages : void 0
867
+ };
868
+ }
869
+ async runSummarizer(e, t, n, r) {
870
+ let { triggerBtn: i, pillsEl: a, progressEl: o, progressSetProgress: c, consentEl: l } = t, { expectedInputLanguages: u, expectedContextLanguages: d } = this.getLanguageHints();
871
+ this.abortController?.abort(), this.abortController = new AbortController();
872
+ let f = this.abortController.signal;
873
+ this.setSummarizingState(t, !0), L(i, "Preparing…"), this.clearSummary(), U(l), G(o);
874
+ let p = n === "downloadable" || n === "downloading", m = null, h = null;
875
+ try {
876
+ p || (h = setTimeout(() => {
877
+ f.aborted || L(i, "Loading model…");
878
+ }, 1200)), m = await g({
879
+ type: e.type,
880
+ length: this.opts.defaultLength,
881
+ format: this.opts.format,
882
+ preference: this.opts.preference,
883
+ sharedContext: this.opts.sharedContext,
884
+ outputLanguage: this.opts.outputLanguage,
885
+ expectedInputLanguages: u,
886
+ expectedContextLanguages: d,
887
+ signal: f,
888
+ onDownloadProgress: p ? (e) => {
889
+ c(e), L(i, `Downloading… ${e.pct}%`);
890
+ } : void 0
891
+ }), h !== null && (clearTimeout(h), h = null), G(o), L(i, "Summarizing…");
892
+ let n = k(this.opts.contentSelector);
893
+ if (!n || n.length < 80) throw Error("Not enough article text found to summarize.");
894
+ let r = K(e.type, this.opts.defaultLength, this.opts.format);
895
+ this.root?.appendChild(r.card), r.card.scrollIntoView({
896
+ behavior: "smooth",
897
+ block: "nearest"
898
+ }), r.stopBtn.addEventListener("click", () => {
899
+ this.abortController?.abort(), r.finalize(), this.setSummarizingState(t, !1), R(i, this.opts.buttonLabel);
900
+ });
901
+ let l = document.title ?? "", _ = await y({
902
+ summarizer: m,
903
+ text: n,
904
+ context: `Article title: "${l}". Provide a clear, accurate summary.`,
905
+ signal: f,
906
+ onChunk: (e) => r.streamChunk(e)
907
+ });
908
+ r.finalize(), a && B(a), R(i, "Regenerate"), _ && this.opts.onSummary?.(_, e.type), await s(this.opts.dbName, e);
909
+ } catch (n) {
910
+ if (n.name === "AbortError") {
911
+ R(i, this.opts.buttonLabel);
912
+ return;
913
+ }
914
+ G(o);
915
+ let a = n instanceof Error ? n : Error(String(n));
916
+ this.opts.onError?.(a), this.showError(`Summary failed: ${a.message || "Unknown error."}`, () => this.preflightAndRun(e, t, r)), R(i, this.opts.buttonLabel);
917
+ } finally {
918
+ h !== null && clearTimeout(h), this.setSummarizingState(t, !1);
919
+ }
920
+ }
921
+ async preflightAndRun(e, t, n) {
922
+ let { expectedInputLanguages: r, expectedContextLanguages: i } = this.getLanguageHints(), a = null;
923
+ try {
924
+ a = await f({
925
+ type: e.type,
926
+ length: this.opts.defaultLength,
927
+ format: this.opts.format,
928
+ preference: this.opts.preference,
929
+ sharedContext: this.opts.sharedContext,
930
+ outputLanguage: this.opts.outputLanguage,
931
+ expectedInputLanguages: r,
932
+ expectedContextLanguages: i
933
+ });
934
+ } catch (e) {
935
+ let t = e instanceof Error ? e : Error(String(e));
936
+ this.showError(`Failed to check model availability: ${t.message}`);
937
+ return;
938
+ }
939
+ if (!a || a === "unavailable") {
940
+ 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>.");
941
+ return;
942
+ }
943
+ if ((a === "downloadable" || a === "downloading") && this.opts.requireDownloadConsent && !n) {
944
+ H(t.consentEl);
945
+ return;
946
+ }
947
+ await this.runSummarizer(e, t, a, n);
948
+ }
949
+ };
950
+ function $() {
951
+ let e = document.querySelector("[data-ai-summarizer]");
952
+ if (!e) return;
953
+ let t = {}, n = e.getAttribute("data-ai-summarizer");
954
+ if (n && n !== "" && n !== "true") try {
955
+ t = JSON.parse(n);
956
+ } catch {}
957
+ new Q(t).mount();
958
+ }
959
+ typeof document < "u" && (document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", $) : $());
960
+ //#endregion
961
+ export { Q as AISummarizer };