cd /news/ai-tools/add-on-device-summarization-to-react… · home › topics › ai-tools › article
[ARTICLE · art-156867] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

Add On-Device Summarization to React with Chrome's Summarizer API

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.

by read10 min views4 publishedOct 24, 2026

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.

That is a useful place for Chrome’s Summarizer API: a bounded reading aid beside an existing text field.

In 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.

This walkthrough builds on commit 4b3f370. 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.

Consider this synthetic brief:

I 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.

A 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.

Keep the original visible beside the output. Label the result “Summary of this brief” and let the person regenerate it after editing.

Continue 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.

The 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.

Chrome 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 and retain the ordinary text workflow.

There are two checks: whether the API exists, and whether it can support the configuration you want.

Use 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.

The examples use JavaScript and JSX. For TypeScript, Chrome recommends @types/dom-chromium-ai. Replace the demo’s handwritten Summarizer global declaration if adopting those types.

In summary-client.js:

export const summaryOptions = {
  type: "key-points",
  format: "plain-text",
  length: "medium",
  expectedInputLanguages: ["en"],
  outputLanguage: "en",
};

export async function checkSummaryAvailability(API = globalThis.Summarizer) {
  if (!API) return "unsupported";
  try {
    const value = await API.availability(summaryOptions);
    return ["available", "downloadable", "down", "unavailable"].includes(value)
      ? value : "unknown";
  } catch {
    return "error";
  }
}

Map the availability result to a useful UI state:

State What the interface should do
Checking Keep the brief editable while checking support
Unsupported or unavailable Explain that on-device summarization cannot run here
Downloadable Offer the action and explain that model setup may require a download
Down Offer to wait for readiness and show progress once attached
Available Offer summarization immediately
Unknown or error Offer a readiness retry and preserve the brief

Run 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.

Chrome’s setup guidance 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.

Availability 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.

For this occasional button action, one instance per request keeps resource ownership straightforward. Create it, generate the summary, and release it in finally.

Add this function to the same module:

export async function summarizeBrief(
  brief,
  { signal, onUpdate = () => {} },
  API = globalThis.Summarizer,
) {
  signal.throwIfAborted();
  if (!API) throw new Error("On-device summarization is unavailable.");
  if (!brief.trim()) throw new Error("Enter a brief first.");

  let session;
  try {
    session = await API.create({
      ...summaryOptions,
      signal,
      monitor(monitor) {
        monitor.addEventListener("downloadprogress", event => {
          if (!signal.aborted) onUpdate({
            phase: "down",
            percent: Math.round(event.loaded * 100),
          });
        }, { signal });
      },
    });
    signal.throwIfAborted();
    onUpdate({ phase: "summarizing" });
    const text = await session.summarize(brief, { signal });
    signal.throwIfAborted();
    return text;
  } finally {
    session?.destroy();
  }
}

The signal goes to both create() 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.

The 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.

Download progress and generation are separate phases. The Writing Assistance API specification 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.

Destroying 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.

Batch 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.

The 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.

A 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.

A dedicated summary component can own one active controller. This hook cancels earlier work and accepts updates only from the currently active request:

import { useEffect, useRef, useState } from "react";
import { summarizeBrief } from "./summary-client.js";

export function useBriefSummary(brief) {
  const active = useRef(null);
  const [view, setView] = useState({ phase: "idle" });

  useEffect(() => () => {
    active.current?.abort();
    active.current = null;
  }, []);

  function cancel() {
    active.current?.abort();
    active.current = null;
    setView({ phase: "cancelled" });
  }

  async function start() {
    active.current?.abort();
    const controller = new AbortController();
    active.current = controller;
    const isCurrent = () =>
      active.current === controller && !controller.signal.aborted;

    setView({ phase: "preparing" });
    try {
      const text = await summarizeBrief(brief, {
        signal: controller.signal,
        onUpdate(update) {
          if (isCurrent()) setView(update);
        },
      });
      if (isCurrent()) setView({ phase: "done", text });
    } catch (error) {
      if (active.current !== controller) return;
      setView({
        phase: controller.signal.aborted ? "cancelled" : "error",
        message: error?.name === "QuotaExceededError"
          ? "This brief is too long. Choose a smaller section."
          : "Could not summarize on this device. Try again or keep reading the brief.",
      });
    } finally {
      if (active.current === controller) active.current = null;
    }
  }

  return { view, start, cancel };
}

The 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.

Trip Workbench already tracks briefVersion, editor generation, and session generation. Use them with the draft ID:

const summarySourceKey = JSON.stringify([
  state.sessionGeneration,
  state.editorGeneration,
  state.savedDraft?.id,
  state.briefVersion,
]);

<BriefSummary
  key={summarySourceKey}
  brief={state.brief}
  readiness={readiness}
/>

React resets component state when its key changes. That removes the previous output, and the hook’s cleanup aborts its request. Keep the textarea in the parent so typing retains focus.

Using 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.

The availability probe can stay in the parent, since changing the brief does not change this example’s fixed language configuration.

Inside BriefSummary, call the hook and derive whether work is in progress:

const { view, start, cancel } = useBriefSummary(brief);
const busy = ["preparing", "down", "summarizing"].includes(view.phase);
const canRun = ["available", "downloadable", "down"].includes(readiness);

return (
  <section aria-label="Brief summary">
    <button
      type="button"
      disabled={!canRun || busy || !brief.trim()}
      onClick={() => void start()}
    >
      Summarize on this device
    </button>
    {busy && <button type="button" onClick={cancel}>Cancel</button>}
    <p role="status">
      {view.phase === "preparing" && "Preparing on-device summarization…"}
      {view.phase === "down" && `Down model: ${view.percent}%`}
      {view.phase === "summarizing" && "Summarizing…"}
      {view.phase === "cancelled" && "Summary cancelled."}
      {view.phase === "done" && "Summary ready."}
      {view.phase === "error" && view.message}
    </p>
    {view.phase === "done" && (
      <>
        <h3>Summary of this brief</h3>
        <p style={{ whiteSpace: "pre-wrap" }}>{view.text}</p>
        <p>Generated on this device. Check details against the original brief.</p>
      </>
    )}
  </section>
);

Place 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.

Render the plain-text result through React text interpolation. The summary does not need HTML insertion.

Long inputs can exceed the model’s input quota. The summarize() API 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.

The 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.

The existing Summarizer unit test checks the missing-API case. That is a useful start, but it does not establish behavior during downloads, cancellation, or editing.

For 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.

These 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.

Use 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.

Then run native browser checks with a small set of briefs:

Case What to inspect
First use on a supported device Readiness, download feedback, cancellation, and eventual completion
Model already ready Creation and generation latency without mixing in first-download time
Brief edited during generation Old output disappears and cannot return
Unsupported configuration A clear explanation and an intact editing workflow
Long brief A recoverable quota response with the original text retained
Travel brief above Dates, negation, conditions, and uncertainty preserved without invented commitments

For 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.

Measure 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.

This 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.

── more in #ai-tools 4 stories · sorted by recency
── more on @chrome 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/add-on-device-summar…] indexed:0 read:10min 2026-10-24 · —