From e1ad9ce8f04585d14207e7b5aa4636a504c5cb8f Mon Sep 17 00:00:00 2001 From: Sebastian Benz Date: Mon, 22 Jul 2024 17:44:44 +0000 Subject: [PATCH] Add gemini nano sample (#1234) Add gemini nano sample --------- Co-authored-by: Oliver Dunk --- .../ai.gemini-on-device/README.md | 14 ++ .../ai.gemini-on-device/background.js | 3 + .../ai.gemini-on-device/images/icon128.png | Bin 0 -> 3949 bytes .../ai.gemini-on-device/images/icon16.png | Bin 0 -> 267 bytes .../ai.gemini-on-device/images/icon32.png | Bin 0 -> 458 bytes .../ai.gemini-on-device/images/icon48.png | Bin 0 -> 890 bytes .../ai.gemini-on-device/manifest.json | 22 +++ .../ai.gemini-on-device/sidepanel/index.css | 82 +++++++++++ .../ai.gemini-on-device/sidepanel/index.html | 38 +++++ .../ai.gemini-on-device/sidepanel/index.js | 138 ++++++++++++++++++ 10 files changed, 297 insertions(+) create mode 100644 functional-samples/ai.gemini-on-device/README.md create mode 100644 functional-samples/ai.gemini-on-device/background.js create mode 100644 functional-samples/ai.gemini-on-device/images/icon128.png create mode 100644 functional-samples/ai.gemini-on-device/images/icon16.png create mode 100644 functional-samples/ai.gemini-on-device/images/icon32.png create mode 100644 functional-samples/ai.gemini-on-device/images/icon48.png create mode 100644 functional-samples/ai.gemini-on-device/manifest.json create mode 100644 functional-samples/ai.gemini-on-device/sidepanel/index.css create mode 100644 functional-samples/ai.gemini-on-device/sidepanel/index.html create mode 100644 functional-samples/ai.gemini-on-device/sidepanel/index.js diff --git a/functional-samples/ai.gemini-on-device/README.md b/functional-samples/ai.gemini-on-device/README.md new file mode 100644 index 00000000..5af48fa9 --- /dev/null +++ b/functional-samples/ai.gemini-on-device/README.md @@ -0,0 +1,14 @@ +# On-device AI with Gemini Nano + +This sample demonstrates how to use the built-in Chrome Prompt API. + +## Overview + +The extension provides a chat interface for the built-in Chrome prompt API. To learn more about the API and how to sign-up for the preview, head over to [Built-in AI on developer.chrome.com](https://developer.chrome.com/docs/ai/built-in). + +## Running this extension + +1. Clone this repository. +2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked). +3. Click the extension icon. +4. Interact with the prompt API in the sidebar. diff --git a/functional-samples/ai.gemini-on-device/background.js b/functional-samples/ai.gemini-on-device/background.js new file mode 100644 index 00000000..d7262b80 --- /dev/null +++ b/functional-samples/ai.gemini-on-device/background.js @@ -0,0 +1,3 @@ +chrome.sidePanel + .setPanelBehavior({ openPanelOnActionClick: true }) + .catch((error) => console.error(error)); diff --git a/functional-samples/ai.gemini-on-device/images/icon128.png b/functional-samples/ai.gemini-on-device/images/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..787906199986752376c877a99165cd7b5a9e1870 GIT binary patch literal 3949 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 P9%Arx^>bP0l+XkK{cvXg literal 0 HcmV?d00001 diff --git a/functional-samples/ai.gemini-on-device/images/icon32.png b/functional-samples/ai.gemini-on-device/images/icon32.png new file mode 100644 index 0000000000000000000000000000000000000000..838c426c8e30870c3a1e2c4f8f83d4614fd85487 GIT binary patch literal 458 zcmV;*0X6=KP)Px$gh@m}R9HvtmrG8=Fc5}kD#3;g*`&Mlk&Dm+u;2!81Wp1sfE#cGxCcrfkytbf z!ls)N)yPpNO2XJq+Kd8HvrA%oK99#U{~)w@((&T*Td5bN)D<@k5#P=bZ@lH?tLd`X zWMDFyznY9%<}ZhFIB~6S1xSMENdert7P0+DfM-7r?@VKi0P7%9#~7~99mXMRi~ZQO zaf0C>egQ}sH$hYYq>XbB1ppUt{%YDu7$l>4Wu0cQtq&&0nP^l4{d2y8bBWd+OA$5fTEecyWYvG^;=N( z_WuGnG%0lEffIm;2YSDTy(gl5EpVpd?0pN6wLAm`@Ok|Kkn!S57o@xvn2v!G&gL$dQTM|`<=c3hFj)ja@%W>`nMgd>kdlEYbR*=|Y;TS)2bIaoNZ;4W(l@h0oWVsYX ztHz|L<9)Vf<=`=iR%=hV7oDyEZqyyqA`Px&G)Y83RA@u(T3v1$F%bT|YFer71M~rcq7o=kAJg;(xk1wt1a1)214KDM1@C0B42d9_`ry0lXOWY$leJzJ>dXNC9Hzxb}_4W$WyUe-RO zK+%uOBme}crrL!v({%zswQ)@Xz!Z1AzyZi&-}Q)T0Nk9315jC?(U=AR06&rbWB@#j zNH5piqK@-Pb;KCx0kGSdbl2@;Q2q>*4ifK zGDn9L7thtTG@i_j(`n^CSf&RqHd~;Vj`Fmc00{e_xqf$v+mAQOg>(t%4F$`gbjH+! zX6yxNPBiO*t7;!t73R2j#?+INXNiiMzZXE<%6oeWfy*j!E6X^3$(b}2^d*~ZV5sB;0^kZ$^%7KzisrpF z{~-LK08D_4e)gEBk31lhmY4pshsZ*_NKBo5P;5(ow*l&oWgVzG@VZ|34aR7xZbPw# QS^xk507*qoM6N<$g15q#s{jB1 literal 0 HcmV?d00001 diff --git a/functional-samples/ai.gemini-on-device/manifest.json b/functional-samples/ai.gemini-on-device/manifest.json new file mode 100644 index 00000000..f5a12bcf --- /dev/null +++ b/functional-samples/ai.gemini-on-device/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Chrome Built-in AI Demo", + "version": "0.1", + "manifest_version": 3, + "description": "Try the built-in AI preview in Chrome.", + "background": { + "service_worker": "background.js" + }, + "permissions": ["sidePanel"], + "side_panel": { + "default_path": "sidepanel/index.html" + }, + "action": { + "default_icon": { + "16": "images/icon16.png", + "32": "images/icon32.png", + "48": "images/icon48.png", + "128": "images/icon128.png" + }, + "default_title": "Open Chat Interface" + } +} diff --git a/functional-samples/ai.gemini-on-device/sidepanel/index.css b/functional-samples/ai.gemini-on-device/sidepanel/index.css new file mode 100644 index 00000000..d88a10d5 --- /dev/null +++ b/functional-samples/ai.gemini-on-device/sidepanel/index.css @@ -0,0 +1,82 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, + Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + color: #1f1f1f; + background-color: #f2f2f2; + font-size: 16px; + padding: 8px; +} + +input, +button, +textarea, +select { + font-family: inherit; + font-size: inherit; +} + +button { + background: #333; + color: white; + border-radius: 8px; + border: none; + min-width: 100px; + padding: 8px; + margin: 16px 0; + cursor: pointer; +} + +button.primary { + background: #333; + color: white; +} + +button.secondary { + background: #ccc; + color: black; +} + +button[disabled] { + background: #ddd; + color: #aaa; +} + +input[type='range'] { + margin-top: 16px; + accent-color: black; +} + +textarea { + --padding: 32px; + width: calc(100% - var(--padding)); + max-width: calc(100% - var(--padding)); +} + +.text, +textarea { + background-color: white; + padding: 16px; + border-radius: 16px; + box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px, rgb(51, 51, 51) 0px 0px 0px 3px; + outline: none; +} + +.blink { + animation: 1s ease-in-out 1s infinite reverse both running blink; +} + +@keyframes blink { + 25% { + opacity: 0.5; + } + 50% { + opacity: 0; + } + 75% { + opacity: 0.5; + } +} + +[hidden] { + display: none; +} diff --git a/functional-samples/ai.gemini-on-device/sidepanel/index.html b/functional-samples/ai.gemini-on-device/sidepanel/index.html new file mode 100644 index 00000000..6c57d155 --- /dev/null +++ b/functional-samples/ai.gemini-on-device/sidepanel/index.html @@ -0,0 +1,38 @@ + + + + + + +

Chrome built-in AI

+ +
+ + +
+
+ + +
+ + + + + + + + diff --git a/functional-samples/ai.gemini-on-device/sidepanel/index.js b/functional-samples/ai.gemini-on-device/sidepanel/index.js new file mode 100644 index 00000000..4be9bed9 --- /dev/null +++ b/functional-samples/ai.gemini-on-device/sidepanel/index.js @@ -0,0 +1,138 @@ +const inputPrompt = document.body.querySelector('#input-prompt'); +const buttonPrompt = document.body.querySelector('#button-prompt'); +const buttonReset = document.body.querySelector('#button-reset'); +const elementResponse = document.body.querySelector('#response'); +const elementLoading = document.body.querySelector('#loading'); +const elementError = document.body.querySelector('#error'); +const sliderTemperature = document.body.querySelector('#temperature'); +const sliderTopK = document.body.querySelector('#top-k'); +const labelTemperature = document.body.querySelector('#label-temperature'); +const labelTopK = document.body.querySelector('#label-top-k'); + +let session; + +async function runPrompt(prompt, params) { + try { + if (!session) { + // Start by checking if it's possible to create a session based on the availability of the model, and the characteristics of the device. + const canCreate = await self.ai.canCreateTextSession(); + // canCreate will be one of the following: + // * "readily": the model is available on-device and so creating will happen quickly + // * "after-download": the model is not available on-device, but the device is capable, + // so creating the session will start the download process (which can take a while). + // * "no": the model is not available for this device. + if (canCreate === 'no') { + console.warn('Built-in prompt API not available.'); + throw new Error( + 'Built-in prompt API not available. Join the preview program to learn how to enable it.' + ); + } + console.log('Creating new text session'); + session = await self.ai.createTextSession(params); + } + return session.prompt(prompt); + } catch (e) { + console.log('Prompt failed'); + console.error(e); + console.log('Prompt:', prompt); + // Reset session + reset(); + throw e; + } +} + +async function reset() { + if (session) { + session.destroy(); + } + session = null; +} + +async function initDefaults() { + const defaults = await window.ai.defaultTextSessionOptions(); + console.log('Model default:', defaults); + sliderTemperature.value = defaults.temperature; + sliderTopK.value = defaults.topK; + labelTopK.textContent = defaults.topK; + labelTemperature.textContent = defaults.temperature; + labelTemperature.value = defaults.temperature; +} + +initDefaults(); + +buttonReset.addEventListener('click', () => { + hide(elementLoading); + hide(elementError); + hide(elementResponse); + reset(); + buttonReset.setAttribute('disabled', ''); +}); + +sliderTemperature.addEventListener('input', (event) => { + labelTemperature.textContent = event.target.value; + reset(); +}); + +sliderTopK.addEventListener('input', (event) => { + labelTopK.textContent = event.target.value; + reset(); +}); + +inputPrompt.addEventListener('input', () => { + if (inputPrompt.value.trim()) { + buttonPrompt.removeAttribute('disabled'); + } else { + buttonPrompt.setAttribute('disabled', ''); + } +}); + +buttonPrompt.addEventListener('click', async () => { + const prompt = inputPrompt.value.trim(); + showLoading(); + try { + const params = { + temperature: sliderTemperature.value, + topK: sliderTopK.value + }; + const response = await runPrompt(prompt, params); + showResponse(response); + } catch (e) { + showError(e); + } +}); + +function showLoading() { + buttonReset.removeAttribute('disabled'); + hide(elementResponse); + hide(elementError); + show(elementLoading); +} + +function showResponse(response) { + hide(elementLoading); + show(elementResponse); + // Make sure to preserve line breaks in the response + elementResponse.textContent = ''; + const paragraphs = response.split(/\r?\n/); + for (const paragraph of paragraphs) { + if (paragraph) { + elementResponse.appendChild(document.createTextNode(paragraph)); + } + elementResponse.appendChild(document.createElement('BR')); + } +} + +function showError(error) { + show(elementError); + hide(elementResponse); + hide(elementLoading); + elementError.textContent = error; +} + +function show(element) { + element.removeAttribute('hidden'); +} + +function hide(element) { + element.setAttribute('hidden', ''); +}