メンバーがChatGPTをどう使いこなしているのか、チームで共有するのに便利なツールをご紹介します。Google Apps Scriptとブックマークレットを活用します。
- メンバーごとにGoogleドキュメントを自動生成して、ChatGPTとの会話を追記していきます。Googleドキュメントの一覧は、Googleスプレッドシートに表示します。
- ChatGPTのウェブ画面 https://chat.openai.com 上でブックマークレットを起動するだけで追記できます。Markdown形式で記録します。以下を流用しています。
事前準備が必要です。誰か一人がApps Scriptでウェブアプリを作成し、利用者は各自でブラウザーにブックマークレットを設定して使います。
事前準備
Google Apps Script
誰か一人だけ作業すればOKです。
- Google Drive上に、会話履歴を保存するフォルダーを作成し、フォルダーのURLをメモしておきます。
- Googleスプレッドシートを作成し、
Doc ID
シートを作ります。 - スプレッドシートの拡張機能から、Apps Scriptを起動し、以下の2つのコード
コード.gs
とIndex.html
を記述します。コード.gs
には、上記でメモしたフォルダーのURLを記載してください。 - Apps Scriptのデプロイで、ウェブアプリとして公開します。ウェブアプリのURLをメモしておきます。
コード.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');
}
Index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<a href="<?= docUrl ?>" target="_blank">Your ChatGPT log</a>
</body>
</html>
ブックマークレット
利用者がそれぞれ作業します。ウェブアプリのURLが必要です。
- 以下のブックマークレットのコードを、ブラウザーのブックマークレットとして保存します。上記でメモしたウェブアプリのURLを記載してください。
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,'&')
.replace(/</g,'<')
.replace(/>/g,'>')
.replace(/"/g,'"');
};
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');
})();