diff --git a/functional-samples/ai.gemini-on-device-summarization/package-lock.json b/functional-samples/ai.gemini-on-device-summarization/package-lock.json index 645a5557..9de7642d 100644 --- a/functional-samples/ai.gemini-on-device-summarization/package-lock.json +++ b/functional-samples/ai.gemini-on-device-summarization/package-lock.json @@ -12,6 +12,8 @@ "@mozilla/readability": "0.5.0", "@rollup/plugin-commonjs": "26.0.1", "@rollup/plugin-node-resolve": "15.2.3", + "dompurify": "^3.1.6", + "marked": "^14.1.2", "rollup": "4.18.1", "rollup-plugin-copy": "^3.5.0" } @@ -562,6 +564,12 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", + "dev": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -952,6 +960,18 @@ "node": ">=12" } }, + "node_modules/marked": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.2.tgz", + "integrity": "sha512-f3r0yqpz31VXiDB/wj9GaOB0a2PRLQl6vJmXiFrniNwjkKdvakqJRULhjFKJpxOchlCRiG5fcacoUZY5Xa6PEQ==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", diff --git a/functional-samples/ai.gemini-on-device-summarization/package.json b/functional-samples/ai.gemini-on-device-summarization/package.json index 9d529cbc..2cd071bd 100644 --- a/functional-samples/ai.gemini-on-device-summarization/package.json +++ b/functional-samples/ai.gemini-on-device-summarization/package.json @@ -13,6 +13,8 @@ "@mozilla/readability": "0.5.0", "@rollup/plugin-commonjs": "26.0.1", "@rollup/plugin-node-resolve": "15.2.3", + "dompurify": "3.1.6", + "marked": "14.1.2", "rollup": "4.18.1", "rollup-plugin-copy": "^3.5.0" } diff --git a/functional-samples/ai.gemini-on-device-summarization/rollup.config.mjs b/functional-samples/ai.gemini-on-device-summarization/rollup.config.mjs index b901a5fe..3b279bf6 100644 --- a/functional-samples/ai.gemini-on-device-summarization/rollup.config.mjs +++ b/functional-samples/ai.gemini-on-device-summarization/rollup.config.mjs @@ -2,22 +2,35 @@ import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import copy from 'rollup-plugin-copy'; -export default { - input: 'scripts/extract-content.js', - output: { - dir: 'dist/scripts', - format: 'cjs' +export default [ + { + input: 'sidepanel/index.js', + output: { + dir: 'dist/sidepanel', + format: 'es', + }, + plugins: [ + commonjs(), + nodeResolve(), + copy({ + targets: [ + { + src: ['manifest.json', 'background.js', 'sidepanel', 'images'], + dest: 'dist' + } + ] + }) + ] }, - plugins: [ - commonjs(), - nodeResolve(), - copy({ - targets: [ - { - src: ['manifest.json', 'background.js', 'sidepanel', 'images'], - dest: 'dist' - } - ] - }) - ] -}; + { + input: 'scripts/extract-content.js', + output: { + dir: 'dist/scripts', + format: 'es' + }, + plugins: [ + commonjs(), + nodeResolve(), + ] + } +]; diff --git a/functional-samples/ai.gemini-on-device-summarization/sidepanel/index.js b/functional-samples/ai.gemini-on-device-summarization/sidepanel/index.js index 242b8c04..77ca65e7 100644 --- a/functional-samples/ai.gemini-on-device-summarization/sidepanel/index.js +++ b/functional-samples/ai.gemini-on-device-summarization/sidepanel/index.js @@ -1,3 +1,6 @@ +import DOMPurify from '../node_modules/dompurify/dist/purify.es.mjs'; +import { marked } from '../node_modules/marked/marked.min'; + // The underlying model has a context of 1,024 tokens, out of which 26 are used by the internal prompt, // leaving about 998 tokens for the input text. Each token corresponds, roughly, to about 4 characters, so 4,000 // is used as a limit to warn the user the content might be too long to summarize. @@ -42,7 +45,7 @@ async function onContentChange(newContent) { async function generateSummary(text) { try { - let session = await createSummarizationSession((message, progress) => { + let session = await createSummarizer((message, progress) => { console.log(`${message} (${progress.loaded}/${progress.total})`); }); let summary = await session.summarize(text); @@ -55,39 +58,32 @@ async function generateSummary(text) { } } -async function createSummarizationSession(downloadProgressCallback) { +async function createSummarizer() { if (!window.ai || !window.ai.summarizer) { throw new Error('AI Summarization is not supported in this browser'); } const canSummarize = await window.ai.summarizer.capabilities(); - if (canSummarize.available === 'no') { - throw new Error('AI Summarization is not availabe'); - } - - const summarizationSession = await window.ai.summarizer.create(); - if (canSummarize.available === 'after-download') { - if (downloadProgressCallback) { - summarizationSession.addEventListener( - 'downloadprogress', - downloadProgressCallback - ); + let summarizer; + if (canSummarize && canSummarize.available !== 'no') { + if (canSummarize.available === 'readily') { + // The summarizer can immediately be used. + summarizer = await window.ai.summarizer.create(); + } else { + // The summarizer can be used after the model download. + summarizer = await window.ai.summarizer.create(); + summarizer.addEventListener('downloadprogress', (e) => { + console.log('Downloading model', e.loaded, e.total); + }); + await summarizer.ready; } - await summarizationSession.ready; + } else { + throw new Error(`AI Summarizer not available (${canSummarize.available})`); } - - return summarizationSession; + return summarizer; } async function showSummary(text) { - // Make sure to preserve line breaks in the response - summaryElement.textContent = ''; - const paragraphs = text.split(/\r?\n/); - for (const paragraph of paragraphs) { - if (paragraph) { - summaryElement.appendChild(document.createTextNode(paragraph)); - } - summaryElement.appendChild(document.createElement('BR')); - } + summaryElement.innerHTML = DOMPurify.sanitize(marked.parse(text)); } async function updateWarning(warning) {