JOURNAL コラム
2024.06.06 エンジニア

ブラウザのテキストエリアでもGitHub Copilotのような文章補完がほしい その2

先日ブラウザ上でGitHub Copilotのような自動補完を実装する記事を公開しました。今回はその続きで、フレームワークを導入して、もう少しエディタらしくしていきたいと思います。

実装

エディタを作成するための手段はたくさんありますが、今回はTipTapというフレームワークを使用します。これはヘッドレスのフレームワークで見た目は用意されておらずエディタとしての機能だけを提供しているフレームワークです。ちなみにTipTapには課金によりAIを使用できる機能があるようですが今回は使用せず、前編で作成した処理を使っていきます。

まずはTipTapを使用する準備をします。今回はSvelteで実装するためSvelteのコンポーネント内でTipTapを呼ぶことで使用します。


 { controlKeyDown(e, editor); }} />

ここではTipTapのコア読み込みとSvelte読み込み時破棄時にそれぞれエディタの作成と破棄を行うように記述しています。また、見た目の調整でTailwindCSSを使用するため、`w-full`などのデザイン用のクラスも付与しています。次にこのコードにも記述しているサジェスト部分のスキーマと関数を定義していきます。まずはスキーマ部分です。

import { Node } from '@tiptap/core';
export const Suggest = Node.create({
    name: 'suggest',
    content: 'inline*',
    marks: '',
    group: 'inline',
    inline: true,
    renderHTML() {
        return [
            'span',
            {
                class: 'suggest',
            },
            0
        ];
    },
})

サジェストは入力している文章と同列にしたいためインライン要素で指定しspan要素にデザイン用のsuggestを付与しています。ここで定義したサジェストをユーザの入力に合わせて追加削除することでCopilot機能を実装します。

次にCopilot機能を実現させるための関数たちを定義していきます。必要なものは大きく分けて、入力時にAIに問い合わせサジェストを取得する機能、サジェストを受け入れる機能になります。まずはサジェストを取得する部分です。取得の要所であるChatGPTへの問い合わせは前回の記事で生やしたAPIを叩くだけなのでシンプルに実装できます。

export const insertSuggest = (editor: any, prompt: string) => {
    if (!document.querySelector('.suggest')) {
        timer = setTimeout(async () => {
            const cursorPosition = editor.state.selection.anchor;
            const beforeText = editor.state.doc.textBetween(0, cursorPosition);
            const afterText = editor.state.doc.textBetween(cursorPosition, editor.state.doc.content.size);
            if (beforeText.length === 0) {
                return;
            }

            const response = await fetch('/api/gpt', {
                method: 'POST',
                body: JSON.stringify({ beforeText: beforeText, afterText: afterText, prompt: prompt}),
                headers: {
                    'Content-Type': 'application/json'
                }
            });
            response.json().then((data) => {
                editor.chain().focus().insertContent({
                    type: "suggest",
                    content: [
                        {
                            type: 'text',
                            text: data.text,
                        },
                    ],
                }).run()
            });
        }, 1000)
    }
}

やっていることはシンプルで関数が実行されたらAPIを叩き、返ってきた文字列をサジェストとしてカーソルがある部分に追加するという動きをしています。これをキー入力事に発火するようにします。ただ入力ごとにAPIを叩いていると攻撃になってしまうので、ここではキー入力から1秒後にAPIを叩くようにして、その1秒間に別のキー入力があればタイマーを破棄して再び1秒を数え直すようにします。

次にサジェスト周りの関数を作ります。

let timer: any = 0;
export const controlKeyDown = (event: any, editor: any, prompt: string) => {
    if (!editor.isFocused) {
        return;
    }
    if (event.key === 'Tab') {
        if(document.querySelector('.suggest')) {
            event.preventDefault();
            commitSuggest(editor);
            removeSuggest();
        }
    } else {
        removeSuggest();
        clearTimeout(timer);
        insertSuggest(editor, prompt);
    }
}
export const commitSuggest = (editor: any) => {
    const text = document.querySelector('.suggest')?.innerHTML;
    editor.chain().focus().insertContent(text).run();
}
export const removeSuggest = () => {
    document.querySelectorAll('.suggest').forEach((el) => {
        el.remove();
    })
}

ここではキー入力に合わせて各種関数実行をコントロールする関数とサジェストを受け入れる関数、サジェストが微妙だった場合に拒否する(サジェストを削除する)関数を定義しています。これらを`<svelte:window on:keydown={(e) => { controlKeyDown(e, editor); }} />`で登録してあげます。


最後に作成したエディタのコンポーネントを読み込んであげれば作業完了です。 実行してみると上記のGIFのように入力に対するサジェストされると思います。スタイルについては本記事で触れていませんが、ここでは”.suggest”に対して文字色だけ設定しています。そしてサジェストが表示された後Tabキーを押すことで確定(文字色が黒に変更)されたかと思います。

終わりに

というわけで2回に分けてGitHub Copilotのような補完機能を実装してきました。元々はブログ執筆の助けになるかなと思い実装してきましたが、実際に使ってみるとサジェスト内にユーザが意図していないGPTくんが書きたい内容が含まれることが多く、改善の余地はありそうです。
とはいえAPIやフレームワークを使うことでここまで簡単に実装することができました。それらの開発者に感謝しつつ改善していきたいと思います。

CCG WORKING HEADSでは、サイト制作やウェブデザインなど、デジタル領域のクリエイティブを軸に、お客様の想いをカタチにします。
お問い合わせから、お気軽にご連絡ください。

このコラムに関連するタグ

Share

  • facebook
  • twitter
  • B!
  • Feedly