Google Apps Scriptとブックマークレットで、ChatGPTとの会話をチームで共有する

メンバーがChatGPTをどう使いこなしているのか、チームで共有するのに便利なツールをご紹介します。Google Apps Scriptとブックマークレットを活用します。

- メンバーごとにGoogleドキュメントを自動生成して、ChatGPTとの会話を追記していきます。Googleドキュメントの一覧は、Googleスプレッドシートに表示します。
- ChatGPTのウェブ画面 https://chat.openai.com 上でブックマークレットを起動するだけで追記できます。Markdown形式で記録します。以下を流用しています。

https://qiita.com/takatama/items/5c187a3c8034b3d099c3

事前準備が必要です。誰か一人がApps Scriptでウェブアプリを作成し、利用者は各自でブラウザーにブックマークレットを設定して使います。

# 事前準備

## Google Apps Script

誰か一人だけ作業すればOKです。

1. Google Drive上に、会話履歴を保存するフォルダーを作成し、フォルダーのURLをメモしておきます。
2. Googleスプレッドシートを作成し、`Doc ID`シートを作ります。
3. スプレッドシートの拡張機能から、Apps Scriptを起動し、以下の2つのコード`コード.gs`と`Index.html`を記述します。`コード.gs`には、上記でメモしたフォルダーのURLを記載してください。
4. Apps Scriptのデプロイで、ウェブアプリとして公開します。ウェブアプリのURLをメモしておきます。

```javascript:コード.gs
const DOC_ID_SHEET_NAME = 'Doc ID';
const FOLDER_URL = 'Google Drive上に作った会話履歴を保存するフォルダーのURLをここに記載します';

function getFolderId_(url) {
  const regex = /https:\/\/drive\.google\.com\/drive\/folders\/([^\/]+)/;
  const match = url.match(regex);
  return match ? match[1] : null;
}

function findDocByEmail(email) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(DOC_ID_SHEET_NAME);
  const values = sheet.getDataRange().getValues();

  const emailColumnIndex = 0;
  const docIdColumnIndex = 1;

  const foundRow = values.find(row => row[emailColumnIndex] === email);
  if (foundRow) return DriveApp.getFileById(foundRow[docIdColumnIndex]);

  const folderId = getFolderId_(FOLDER_URL);
  const newDoc = DocumentApp.create(email);
  const newDocId = newDoc.getId();

  const file = DriveApp.getFileById(newDocId);
  const folder = DriveApp.getFolderById(folderId);
  file.moveTo(folder);

  sheet.appendRow([email, newDocId]);
  console.log('new docId', email, newDocId);
  return newDoc;
}

function getDiff(text, lastParagraph) {
  const i = text.indexOf(lastParagraph);
  if (i === -1) return text.trim();
  return text.slice(i + lastParagraph.length).trim();
}

function appendTitleAndText(docId, title, text) {
  const doc = DocumentApp.openById(docId);
  const body = doc.getBody();
  const paragraphs = body.getParagraphs();
  const lastParagraph = paragraphs[paragraphs.length - 1].getText().replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();

  // Replace multiple types of line breaks with '\n'
  text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
  
  const diff = getDiff(text, lastParagraph);
  if (!diff) return;
  
  const dateString = Utilities.formatDate(new Date(), 'JST', 'yyyy/MM/dd');
  const header = body.appendParagraph(`# ${dateString} ${title}`);
  header.setHeading(DocumentApp.ParagraphHeading.HEADING1);

  body.appendParagraph(diff);
}

function createResponse_(obj) {
  const output = ContentService.createTextOutput(JSON.stringify(obj));
  output.setMimeType(ContentService.MimeType.JSON);
  return output;
}

function doPost(e) {
  const params = JSON.parse(e.postData.contents);
  const { title, text } = params;

  const email = Session.getActiveUser().getEmail();
  const docId = findDocByEmail(email).getId();

  if (!docId) {
    return createResponse_({ 'result': 'error', 'message': 'No document found for this user.' });
  }

  appendTitleAndText(docId, title, text);
  return createResponse_({ 'result': 'success', 'message': 'Successfully appended data.' });
}

function doGet() {
  const email = Session.getActiveUser().getEmail();
  const docUrl = findDocByEmail(email).getUrl();
  const template = HtmlService.createTemplateFromFile('Index');
  template.docUrl = docUrl;
  const output = template.evaluate();
  return output.setTitle('ChatGPT Log');
}

```

```html:Index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <a href="<?= docUrl ?>" target="_blank">Your ChatGPT log</a>
  </body>
</html>
```

## ブックマークレット

利用者がそれぞれ作業します。ウェブアプリのURLが必要です。

1. 以下のブックマークレットのコードを、ブラウザーのブックマークレットとして保存します。上記でメモしたウェブアプリのURLを記載してください。

```javascript:bookmarklet
javascript:(function() {
  const URL = 'Apps Scriptで作ったウェブアプリのURLをここに記載します';
  function post(title, text) {
    fetch(URL, {
      method: 'post',
      mode: 'no-cors',
      credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({title, text})
    })
    .then(res => console.log(res))
    .catch(err => console.error(err));
  }
  if (!document.URL.startsWith('https://chat.openai.com')) {
    alert('Please use on https://chat.openai.com');
    return;
  }
  const sanitize = html => {
    return String(html).replace(/&/g,'&amp;')
      .replace(/</g,'&lt;')
      .replace(/>/g,'&gt;')
      .replace(/"/g,'&quot;');
  };
  const table = element => {
    const rows = [...element.rows].map(row => [...row.cells].map(cell => cell.innerText));
    const header = rows.shift();
    const separator = header.map(() => '---');
    const body = rows.map(row => `| ${row.join(' | ')} |`);
    return `| ${header.join(' | ')} |\n|${separator.join('|')}|\n${body.join('\n')}`;
  };
  const escapeTags = text => {
    return String(text).replace(/<(\/?[a-zA-Z]+)>/g, '`<$1>`');
  };
　const content = element => {
    const tag = element.tagName;
    if (tag === 'OL') return [...element.querySelectorAll('li')].map((li, i) => `${i+1}. ${li.innerText}`).join('\n');
    if (tag === 'UL') return [...element.querySelectorAll('li')].map(li => `- ${li.innerText}`).join('\n');
    if (tag === 'PRE') {
      const lang = element.querySelector('span').innerText;
      return '```' + lang + '\n' + element.querySelector('code').innerText + '```';
    }
    if (tag === 'TABLE') return table(element);
    return escapeTags(element.innerText);
  };
  const talks = [...document.querySelectorAll('.text-base')];
  const title = talks.length % 2 === 0 ? '' : sanitize(`# ${talks.shift().innerText}\n\n`);
  const text = talks.map((talk, i) => {
    const who = i % 2 === 0 ? 'あなた' : 'ChatGPT';
    const elements = talk.querySelectorAll('p,ol,ul,pre,table');
    const c = elements.length === 0 ? talk.innerText : [...elements].map(e => content(e)).join('\n\n');
    return `## ${who}:\n${c}`;
  }).join('\n\n');
  post(title, text);
  const win = window.open(URL, 'target', 'width=200,height=150'); 
})();
```