前回の概要と今回の目的
前回の記事で ChatGPT のチャット履歴をローカルに記録する方法について検討した。その結果、チャット履歴を手動で記録、装飾を施す方針に決定した。形式は HTML と CSS を組み合わせ、SSG のフレームワークとしてデフォルトでは JavaScript を生成しない Astro を使用するところまで決めた。今回は Astro を使った実装について書いていく。
Astro の導入
まず、Astro の公式チュートリアルに沿って環境を構築した。なお、チュートリアルの記事には現行のバージョンでは非推奨となっている内容 [F1] も含まれているため注意が必要だ。
パッケージマネージャ
Astro は Node パッケージマネージャとして npm と pnpm、Yarn に対応している。ここでは軽量な pnpm を使用した。
Astro のバージョン
Astro のバージョンは 5.5.4 [F2] をNode.js は v20.18.1 で pnpm は 9.15.3 を想定している。
インストールとセットアップ
セットアップは以下のコマンドで行う。詳細は公式サイトの「Astroのインストールとセットアップ」を確認してほしい。
pnpm create astro@latest 手動でチャット履歴を取得して記録する方法
手動でコピー&ペースト
履歴の記録には、ChatGPT のページから手動でコピー&ペーストする方法を採用した。これは自動的に履歴を取得するいい方法が見つからなかったためだ。やり方は実際に ChatGPT のページでチャットを開き、そこに表示されているテキストをコピーしてエディタにペーストするだけだ。
パーツのコンポーネント化
手動での作業は非常に時間と労力がかかる。そこで HTML タグの記述など共通化できるパーツは Astro のコンポーネントとして作成した。これで手動で入力する作業が減るだけでなく、パーツの共通化によって仕様変更時のコード修正が少なく済むため、保守性が向上する。
HTML タグの記述に関する制約
パーツをコンポーネントにする過程でコードフェンス内 [F3] にチャット履歴を記述することになった。この部分では HTML
タグを直接書けない制約がある。これを解決するために HTML タグを string 型で生成する関数を作成した。以下に <p> を追加するコードの例を書く。
// <> を置き換える処理
const sanitize = (children: string): string =>
children.replaceAll('<', '<').replaceAll('>', '>');
// ChatGPT の回答内容に問題があった場合、それに応じたクラスを付与する
const getType = (type: TextType): string | undefined => {
switch (type) {
case TextType.Annot: // 補足情報を強調したい場合
return 'annotation';
case TextType.Intrp: // 回答の生成が途中で停止した場合
return 'interruption';
case TextType.Wrong: // 回答内容に誤りがある場合
return 'wrong';
default:
return undefined;
}
};
// 与えられた要素をタグで囲う関数
const addElement = (e: string, c: string, s: boolean, t?: TextType): string => {
c = s ? sanitize(c) : c;
const cn = t ? getType(t) : undefined;
return cn ? `<${e} class="${cn}">${c}</${e}>` : `<${e}>${c}</${e}>`;
};
// 与えられた要素を特定のタグで囲う関数を作成する関数
const createAddElement = (element: string, sanitize: boolean) => {
return (children: string, type?: TextType): string =>
addElement(element, children, sanitize, type);
};
const addParagraph = createAddElement('p', false); // <p> タグを追加する関数
export const addCode = createAddElement('code', true); // <code> タグを追加する関数
const addSpan = createAddElement('span', false); // <span> タグを追加する関数
const text1 = addCode('Hello -> '); // <code>Hello -> </code>
const text2 = addSpan('Wrold', TextType.Wrong); // <span class="wrong">Wrold</span>
addParagraph(`${text1}${text2}`); // <p><code>Hello -> </code><span class="wrong">Wrold</span></p> 仕様
ファイル名とディレクトリ構成
記事の管理は、 src/pages/articles ディレクトリ内に YYYYMM 形式で新たなディレクトリを作成する方法を採用した。各記事のファイル名は、記事のタイトルを
lowerCamelCase の英語にしたものとする。
記事のフォーマット
記事はそれぞれ、チャット履歴とメタデータから構成される。
チャット履歴
各チャットは IntfChat 形式で記録する。
interface IntfChat {
user: string; // ユーザの入力
gpt: string; // GPT の回答
model: string; // 回答に使用されたモデル
memory?: string; // メモリに追加された内容
} chats を IntfChat[] で定義することにより連続する会話を記録することができる。
const chats: IntfChat[] = [
{
user: ユーザの入力,
gpt: GPT の回答,
model: モデル名,
},
{
user: ユーザの入力,
gpt: GPT の回答,
model: モデル名,
memory?: メモリに追加された内容
}
]; メタデータ
基本的な記事のメタデータを IntfFrontmatter 型の定数 frontmatter として定義する。
interface IntfFrontmatter {
title: string; // タイトル
pageDates: PageDates; // 作成日と更新日
description: string; // 説明
tags: string[]; // タグ
} 実際に frontmatter を定義する。
export const frontmatter: IntfFrontmatter = {
title: 'Forgotten Skillet Name',
pageDates: new PageDates('2024-07-01', '2025-02-21'),
description: 'キャンプで使うフライパンみたいな鉄製のカタカナの調理器具の名前が思い出せません',
tags: ['食事・料理']
}; 問題点と改善点
ChatGPT の機能更新への対応
この記録は2024年6月に始めたもので、2025年3月現在までに ChatGPT には画像の生成機能や推論モデルの追加など多くの更新がなされた。現在の実装ではこの更新に対応できていない部分が多く、ほとんどは表示に関する部分だ。これらの機能に対応できない限りはチャットの履歴を記録することができない。そのため、できる限り早く追従することが必要だと考えている。容量問題がある画像の表示以外の機能は実装可能であり、近く対応させる予定だ。
対応予定の機能
現時点でこれだけの機能が実装されていない。
- 数式の表示
- 推論モデルが推論する過程の表示
- ソースコードの言語名の表記
- ユーザがアップロードした画像や ChatGPT が生成した画像の表示
- 検索、参考にしたサイトのリストアップ
自動化の可能性
ChatGPT はアップデートでより長く複雑な回答ができるようになっている。既存の手動作業では記録の精度や速度に限界がある。今後は作業の高速化と効率化を図るべく、エクスポートした JSON データから自動的に Markdown へ変換する機能の実装も考えている。
次回の内容
次回は記事を分類するカテゴリとタグの比較、タグの実装について考える。