🚀

AIで英語の技術文書を超高速要約。Chrome拡張機能「Sidekick AI」を作りました!

に公開

はじめに

エンジニアの日常において、英語の技術文書を読む機会は避けられません。GitHubのREADME、arXivの論文、Hacker Newsの議論、あるいは公式ドキュメントなど、価値ある情報の多くは英語で発信されています。しかし、長文の英語ドキュメントを前にすると、どうしても心理的なハードルを感じてしまうことはないでしょうか。

「このドキュメントは今読むべきか?」「結論だけ先に知りたい」といった課題を解決するため、Sidekick AIというChrome拡張機能を開発しました。

本記事では、Sidekick AIの機能紹介とともに、Chrome Side Panel APIやOpenAI APIを活用した実装の裏側、特にこだわったポイントについて解説します。

GitHub - cmc-labo/sidekick-ai

Sidekick AI とは

Sidekick AIは、現在開いているWebページ(英語の技術文書)をOpenAI APIを用いて要約し、Chromeのサイドパネルに表示する拡張機能です。

主な特徴は以下の通りです。

  • 3行要約のストリーミング表示: ページの内容を「結論」「背景」「ネクストアクション」の3つの項目に整理し、リアルタイムにストリーミング表示します。
  • 文脈を考慮したQ&A: 要約を読んだ後、そのページの内容に基づいた追加の質問をチャット形式で行うことができます。
  • マルチ言語対応: UIと要約結果の出力言語を、英語、日本語、中国語から一括で切り替え可能です。
  • ローカル履歴保存: 過去の要約はすべてローカル(chrome.storage.local)に保存され、後から検索したり、Markdown形式でエクスポートしたりできます。

リポジトリはこちらです。
GitHub - cmc-labo/sidekick-ai

使用例

技術スタック

本プロジェクトは、外部ライブラリに依存しない軽量な構成を採用しています。

技術要素 概要
プラットフォーム Chrome Extension (Manifest V3)
主要API Chrome Side Panel API, Storage API, Scripting API
AI連携 OpenAI API (Chat Completions, Streaming)
言語 Vanilla JavaScript, HTML, CSS

ReactやVueなどのフレームワークを使用せず、Vanilla JSで実装することで、拡張機能自体のファイルサイズを抑え、高速な起動を実現しています。

実装のポイント

エンジニア向けのツールとして、特に「体感速度」と「情報の正確性」にこだわって実装しました。

1. サイト固有のコンテンツ抽出(Site-aware Extraction)

Webページ全体をそのままLLMに投げると、ナビゲーションメニューやフッターなどの不要な情報がノイズとなり、要約の精度が低下します。また、トークン数の無駄遣いにもなります。

そこで、content.jsにて、技術者がよく閲覧する特定のドメインに対して最適化したDOM抽出ロジックを実装しました。

例えば、GitHubやarXivの場合は以下のように特定のセレクタを狙い撃ちしています。

// content.js の一部抜粋
if (url.includes('github.com')) {
  // READMEの場合
  if (/github\.com\/[^/]+\/[^/]+\/?$/.test(url)) {
    const el = document.querySelector('#readme .markdown-body, article.markdown-body');
    text = el?.innerText ?? '';
  }
  // Issue / Pull Requestの場合
  else if (url.includes('/issues/') || url.includes('/pull/')) {
    const bodies = document.querySelectorAll('.comment-body, .js-comment-body, .markdown-body');
    text = Array.from(bodies).map((el) => el.innerText.trim()).join('\n\n');
  }
} else if (url.includes('arxiv.org')) {
  // arXivのAbstract抽出
  const abstract = document.querySelector('blockquote.abstract') ?? document.querySelector('.abstract');
  text = abstract?.innerText.replace(/^Abstract:\s*/i, '').trim() ?? '';
}

特定のドメインに合致しない一般的なブログ記事などの場合は、<article><main>.post-contentといった汎用的なセレクタをフォールバックとして使用し、最終手段としてdocument.body.innerTextを取得する多段構えの設計にしています。

2. トークン制限と文脈維持のトレードオフ

抽出したテキストが長すぎる場合、APIのトークン制限に引っかかる可能性があります。Sidekick AIでは、抽出したテキストを最大3,000文字に制限しています。

ここで工夫したのが、「頭と尻」を残すというアプローチです。技術文書において、重要な情報は「導入(背景)」と「結論(まとめ)」に集中していることが多いためです。

// content.js の一部抜粋
const MAX_CHARS = 3000;
if (text.length > MAX_CHARS) {
  const half = Math.floor(MAX_CHARS / 2);
  text = text.slice(0, half) + '\n\n...[CONTENT TRUNCATED]...\n\n' + text.slice(-half);
}

この処理により、長大なドキュメントであっても、LLMが全体像を把握しやすくなり、要約の精度が向上します。

3. ストリーミングAPIによる体感速度の向上

要約結果を待つ時間は、ユーザー体験を大きく損ないます。OpenAI APIのストリーミングレスポンスを活用し、文字が生成されるたびにUIを更新する実装を行いました。

fetch APIのReadableStreamを処理するロジックは以下のようになっています。

// sidepanel.js の一部抜粋
const response = await fetch(OPENAI_API_URL, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`
  },
  body: JSON.stringify({
    model: selectedModel,
    messages: messages,
    stream: true
  })
});

const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  
  const chunk = decoder.decode(value, { stream: true });
  const lines = chunk.split('\n').filter(line => line.trim() !== '');
  
  for (const line of lines) {
    if (line === 'data: [DONE]') return;
    if (line.startsWith('data: ')) {
      const data = JSON.parse(line.slice(6));
      const content = data.choices[0].delta.content;
      if (content) {
        // UIに文字を追加してレンダリング
        appendAndRender(content);
      }
    }
  }
}

この実装により、ボタンをクリックしてから数秒以内に最初の文字が表示され始め、ユーザーは待ち時間を感じることなく要約を読み始めることができます。

4. 状態管理と多言語対応(i18n)

Sidekick AIは、英語、日本語、中国語の出力に対応しています。この言語設定は、単にLLMへのプロンプトを変更するだけでなく、拡張機能のUI全体(ボタンのラベルやプレースホルダーなど)にも即座に反映されるように設計されています。

chrome.storage.onChangedイベントをリッスンすることで、オプションページで言語設定が変更された瞬間に、サイドパネル側のUIも再レンダリングされる仕組みです。

// sidepanel.js の一部抜粋
chrome.storage.onChanged.addListener((changes, area) => {
  if (area === 'local' && changes.output_language) {
    applyUIStrings(changes.output_language.newValue);
  }
});

また、履歴データには「どの言語で要約されたか」というメタデータも一緒に保存しているため、過去の履歴を開いた際にも、当時の言語に応じた適切なラベル(例:「Conclusion」や「結論」)が表示されるようになっています。

おわりに

Sidekick AIは、自分自身が「もっと効率的に英語の技術記事を読みたい」という動機から開発をスタートしました。Chrome Side Panel APIとLLMを組み合わせることで、ブラウジング体験を妨げることなく、強力なアシスタントを常駐させることができます。

Vanilla JSでの実装は、フレームワークの恩恵を受けられない分、DOM操作や状態管理を自前で書く必要がありますが、ブラウザ拡張機能の仕組みやAPIの挙動を深く理解する良い機会となりました。

リポジトリは公開していますので、興味のある方はぜひインストールして試してみてください。IssueやPull Requestもお待ちしております!

GitHub - cmc-labo/sidekick-ai

Discussion