{"slug": "add-on-device-summarization-to-react-with-chrome-s-summarizer-api", "title": "Add On-Device Summarization to React with Chrome's Summarizer API", "summary": "A developer built a local summarization feature into the Trip Workbench demo using Chrome's on-device Summarizer API, adding capability-state checks, download feedback, cancellation, and a guard that prevents a stale summary from appearing beside an edited brief. The implementation keeps the original brief visible, sends the full original text rather than the lossy summary to the separate Gemini field-extraction step, and requires no application API key for the local inference path. Chrome documents Summarizer support from version 138, though the API remains limited-availability and depends on platform, hardware, storage, and model availability.", "body_md": "A travel brief often mixes the trip itself with meeting context, preferences, and unresolved decisions. A short summary can help the person review it before requesting proposed form changes.\n\nThat is a useful place for Chrome’s Summarizer API: a bounded reading aid beside an existing text field.\n\nIn [Trip Workbench](https://github.com/rajudandigam/trip-workbench), the public demo I built and maintain, the original brief already feeds a separate Gemini feature that proposes trip fields for review. The local summary has a different purpose. It helps the person read the brief; it does not update the trip, replace the source text, or decide which details the cloud model receives.\n\nThis walkthrough builds on [commit `4b3f370`](https://github.com/rajudandigam/trip-workbench/tree/4b3f37034fc291ad5221969e0507d349878dc150). The demo has the basic API call. The lifecycle extensions below add capability states, download feedback, cancellation, and a boundary that prevents an old summary from appearing beside a changed brief.\n\nConsider this synthetic brief:\n\nI need to attend a customer workshop in New York on November 12–13, 2026. Depart SFO for JFK on November 11. Economy, one traveler. Do not book a hotel because I will stay with family. Travel only if the client confirms the workshop by Friday. Plan to return November 14, but November 15 is still possible if the workshop extends.\n\nA useful summary would preserve the dates, the hotel exclusion, the confirmation condition, and the uncertainty around the return. A fluent summary that converts those details into a confirmed itinerary would be misleading.\n\nKeep the original visible beside the output. Label the result “Summary of this brief” and let the person regenerate it after editing.\n\nContinue sending the **original brief** to Gemini when the person selects “Suggest trip details.” Using a lossy summary as the extraction input would make any omitted condition harder for the next step to recover.\n\nThe feature needs no application API key or `@google/genai` call. Chrome supplies the on-device model. Chrome documents that model use does not send the input to Google or a third party, while initial model setup can require a download. That statement applies to the local inference path; the application’s separate Gemini request remains a cloud operation. See [Chrome’s Summarizer guide](https://developer.chrome.com/docs/ai/summarizer-api).\n\nChrome documents Summarizer support from version 138, subject to platform, hardware, storage, and model availability. It remains a limited-availability web API, so browser version alone is insufficient. Serve the feature in a [secure context](https://developer.mozilla.org/en-US/docs/Web/API/Summarizer) and retain the ordinary text workflow.\n\nThere are two checks: whether the API exists, and whether it can support the configuration you want.\n\nUse the same core options for checking availability and creating the instance. This example requests English key points in plain text. The existing demo uses a short `tldr`; medium key points give this review task more room without guaranteeing that every detail survives.\n\nThe examples use JavaScript and JSX. For TypeScript, Chrome recommends [`@types/dom-chromium-ai`](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/dom-chromium-ai). Replace the demo’s handwritten Summarizer global declaration if adopting those types.\n\nIn `summary-client.js`:\n\n``` js\nexport const summaryOptions = {\n  type: \"key-points\",\n  format: \"plain-text\",\n  length: \"medium\",\n  expectedInputLanguages: [\"en\"],\n  outputLanguage: \"en\",\n};\n\nexport async function checkSummaryAvailability(API = globalThis.Summarizer) {\n  if (!API) return \"unsupported\";\n  try {\n    const value = await API.availability(summaryOptions);\n    return [\"available\", \"downloadable\", \"downloading\", \"unavailable\"].includes(value)\n      ? value : \"unknown\";\n  } catch {\n    return \"error\";\n  }\n}\n```\n\nMap the [availability result](https://developer.mozilla.org/en-US/docs/Web/API/Summarizer/availability_static) to a useful UI state:\n\n| State | What the interface should do | \n|---|---|\n| Checking | Keep the brief editable while checking support | \n| Unsupported or unavailable | Explain that on-device summarization cannot run here | \n| Downloadable | Offer the action and explain that model setup may require a download | \n| Downloading | Offer to wait for readiness and show progress once attached | \n| Available | Offer summarization immediately | \n| Unknown or error | Offer a readiness retry and preserve the brief | \n\nRun this capability check in a parent Effect with a cleanup guard so a late result cannot update an abandoned panel. Do not call `create()` from that Effect.\n\nChrome’s [setup guidance](https://developer.chrome.com/docs/ai/get-started) requires user interaction when model downloads are needed. Let the Summarize button call creation directly. Separating the readiness probe also keeps a potentially slow check out of the click path.\n\nAvailability can change. A successful probe is not a reservation; creation still needs error handling. When the language or summary configuration changes, check the new options again.\n\nFor this occasional button action, one instance per request keeps resource ownership straightforward. Create it, generate the summary, and release it in `finally`.\n\nAdd this function to the same module:\n\n``` js\nexport async function summarizeBrief(\n  brief,\n  { signal, onUpdate = () => {} },\n  API = globalThis.Summarizer,\n) {\n  signal.throwIfAborted();\n  if (!API) throw new Error(\"On-device summarization is unavailable.\");\n  if (!brief.trim()) throw new Error(\"Enter a brief first.\");\n\n  let session;\n  try {\n    session = await API.create({\n      ...summaryOptions,\n      signal,\n      monitor(monitor) {\n        monitor.addEventListener(\"downloadprogress\", event => {\n          if (!signal.aborted) onUpdate({\n            phase: \"downloading\",\n            percent: Math.round(event.loaded * 100),\n          });\n        }, { signal });\n      },\n    });\n    signal.throwIfAborted();\n    onUpdate({ phase: \"summarizing\" });\n    const text = await session.summarize(brief, { signal });\n    signal.throwIfAborted();\n    return text;\n  } finally {\n    session?.destroy();\n  }\n}\n```\n\nThe signal goes to both [`create()`](https://developer.mozilla.org/en-US/docs/Web/API/Summarizer/create_static) and `summarize()`. Cancelling while creation is pending must reach that operation, too. Aborting a creation signal after it resolves also destroys the resulting instance, so this signal belongs to this one request.\n\nThe final abort check prevents a late result from being accepted after cancellation. The `finally` block releases any instance that was acquired, including when generation fails.\n\nDownload progress and generation are separate phases. The [Writing Assistance API specification](https://webmachinelearning.github.io/writing-assistance-apis/) defines download progress as a fraction with a total of 1. A displayed 100% means the download stage reached completion; the interface should still wait for instance creation and summarization to finish.\n\nDestroying an instance releases its resources. It does not mean that the browser removes its downloaded model, nor does this code promise to cancel a shared download for every other page.\n\nBatch output keeps this first implementation small. Streaming can improve perceived responsiveness for longer output, but it also requires managing incomplete text and clearing partial results when the request becomes stale.\n\nThe current demo creates an AbortController inside its click handler but does not retain it for cancellation. It also stores the resulting text without binding it to a brief version.\n\nA person can therefore edit the brief while a summary is running, or start another request before the first finishes. The last response to arrive can overwrite the display even when it belongs to older input.\n\nA dedicated summary component can own one active controller. This hook cancels earlier work and accepts updates only from the currently active request:\n\n``` js\nimport { useEffect, useRef, useState } from \"react\";\nimport { summarizeBrief } from \"./summary-client.js\";\n\nexport function useBriefSummary(brief) {\n  const active = useRef(null);\n  const [view, setView] = useState({ phase: \"idle\" });\n\n  useEffect(() => () => {\n    active.current?.abort();\n    active.current = null;\n  }, []);\n\n  function cancel() {\n    active.current?.abort();\n    active.current = null;\n    setView({ phase: \"cancelled\" });\n  }\n\n  async function start() {\n    active.current?.abort();\n    const controller = new AbortController();\n    active.current = controller;\n    const isCurrent = () =>\n      active.current === controller && !controller.signal.aborted;\n\n    setView({ phase: \"preparing\" });\n    try {\n      const text = await summarizeBrief(brief, {\n        signal: controller.signal,\n        onUpdate(update) {\n          if (isCurrent()) setView(update);\n        },\n      });\n      if (isCurrent()) setView({ phase: \"done\", text });\n    } catch (error) {\n      if (active.current !== controller) return;\n      setView({\n        phase: controller.signal.aborted ? \"cancelled\" : \"error\",\n        message: error?.name === \"QuotaExceededError\"\n          ? \"This brief is too long. Choose a smaller section.\"\n          : \"Could not summarize on this device. Try again or keep reading the brief.\",\n      });\n    } finally {\n      if (active.current === controller) active.current = null;\n    }\n  }\n\n  return { view, start, cancel };\n}\n```\n\nThe hook controls request ordering within one mounted summary component. Bind that component’s identity to the source so an edit also clears an already completed summary.\n\nTrip Workbench already tracks `briefVersion`, editor generation, and session generation. Use them with the draft ID:\n\n``` js\nconst summarySourceKey = JSON.stringify([\n  state.sessionGeneration,\n  state.editorGeneration,\n  state.savedDraft?.id,\n  state.briefVersion,\n]);\n\n<BriefSummary\n  key={summarySourceKey}\n  brief={state.brief}\n  readiness={readiness}\n/>\n```\n\nReact [resets component state when its key changes](https://react.dev/learn/preserving-and-resetting-state). That removes the previous output, and the hook’s cleanup aborts its request. Keep the textarea in the parent so typing retains focus.\n\nUsing the existing brief version also invalidates a result when the person edits and then restores the same text. If summary options become editable, include their version in this identity as well.\n\nThe availability probe can stay in the parent, since changing the brief does not change this example’s fixed language configuration.\n\nInside `BriefSummary`, call the hook and derive whether work is in progress:\n\n``` js\nconst { view, start, cancel } = useBriefSummary(brief);\nconst busy = [\"preparing\", \"downloading\", \"summarizing\"].includes(view.phase);\nconst canRun = [\"available\", \"downloadable\", \"downloading\"].includes(readiness);\n\nreturn (\n  <section aria-label=\"Brief summary\">\n    <button\n      type=\"button\"\n      disabled={!canRun || busy || !brief.trim()}\n      onClick={() => void start()}\n    >\n      Summarize on this device\n    </button>\n    {busy && <button type=\"button\" onClick={cancel}>Cancel</button>}\n    <p role=\"status\">\n      {view.phase === \"preparing\" && \"Preparing on-device summarization…\"}\n      {view.phase === \"downloading\" && `Downloading model: ${view.percent}%`}\n      {view.phase === \"summarizing\" && \"Summarizing…\"}\n      {view.phase === \"cancelled\" && \"Summary cancelled.\"}\n      {view.phase === \"done\" && \"Summary ready.\"}\n      {view.phase === \"error\" && view.message}\n    </p>\n    {view.phase === \"done\" && (\n      <>\n        <h3>Summary of this brief</h3>\n        <p style={{ whiteSpace: \"pre-wrap\" }}>{view.text}</p>\n        <p>Generated on this device. Check details against the original brief.</p>\n      </>\n    )}\n  </section>\n);\n```\n\nPlace the readiness explanation and a readiness-retry button beside this control in the parent. A disabled action should have a reason the person can read.\n\nRender the plain-text result through React text interpolation. The summary does not need HTML insertion.\n\nLong inputs can exceed the model’s input quota. The [`summarize()` API](https://developer.mozilla.org/en-US/docs/Web/API/Summarizer/summarize) can reject with `QuotaExceededError`; the hook turns that into a request to choose a smaller section. For a document-oriented feature, `measureInputUsage()` and `inputQuota` can support an earlier check. A character count is not an exact token or quota measurement.\n\nThe fallback remains useful: the person can read and edit the original brief, then use the rest of the form. A cloud summary could be a separate action with a clear explanation that it sends the text to a server. Failure of the local path should not silently trigger that transfer.\n\nThe existing [Summarizer unit test](https://github.com/rajudandigam/trip-workbench/blob/4b3f37034fc291ad5221969e0507d349878dc150/tests/unit/summarizer.test.ts) checks the missing-API case. That is a useful start, but it does not establish behavior during downloads, cancellation, or editing.\n\nFor the adapter shown here, eight controlled checks pass with a fake Summarizer. They cover availability states, availability errors, cancellation before creation, immediate creation in the call path, cancellation during creation, progress and input forwarding, rejected late output, and cleanup after generation errors.\n\nThese are lifecycle results. The React wiring has not been mounted in a browser during this walkthrough, and no native model output or performance measurement is claimed.\n\nUse component tests for replacement requests, brief changes, draft changes, unmounting, and Strict Mode cleanup. Assert that an old result never appears beside a new source. Keep those tests deterministic with deferred promises.\n\nThen run native browser checks with a small set of briefs:\n\n| Case | What to inspect | \n|---|---|\n| First use on a supported device | Readiness, download feedback, cancellation, and eventual completion | \n| Model already ready | Creation and generation latency without mixing in first-download time | \n| Brief edited during generation | Old output disappears and cannot return | \n| Unsupported configuration | A clear explanation and an intact editing workflow | \n| Long brief | A recoverable quota response with the original text retained | \n| Travel brief above | Dates, negation, conditions, and uncertainty preserved without invented commitments | \n\nFor the travel brief, record whether the output preserves “do not book a hotel,” the client-confirmation condition, and the uncertain return date. These are evaluation criteria, not recorded model results.\n\nMeasure correctness alongside usefulness: how long it takes the person to review the summary and source, how often important details are omitted, and whether the summary saves effort at all. Very short briefs may be faster to read directly.\n\nThis gives the feature a clear role in the wider assistant: a local reading aid, tied to one version of the source, that leaves the original available for human review and the later Gemini proposal.", "url": "https://wpnews.pro/news/add-on-device-summarization-to-react-with-chrome-s-summarizer-api", "canonical_source": "https://dev.to/raju_dandigam/add-on-device-summarization-to-react-with-chromes-summarizer-api-1bkn", "published_at": "2026-10-24 16:44:16+00:00", "updated_at": "2026-10-24 16:46:30.062854+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "artificial-intelligence", "ai-products"], "entities": ["Chrome", "Google", "Gemini", "Trip Workbench", "Summarizer API"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/add-on-device-summarization-to-react-with-chrome-s-summarizer-api", "markdown": "https://wpnews.pro/news/add-on-device-summarization-to-react-with-chrome-s-summarizer-api.md", "text": "https://wpnews.pro/news/add-on-device-summarization-to-react-with-chrome-s-summarizer-api.txt", "jsonld": "https://wpnews.pro/news/add-on-device-summarization-to-react-with-chrome-s-summarizer-api.jsonld"}}