Svelte で ChatGPT 風なチャット画面を作成した

# はじめに

以下の投稿では入力ボックスだけを作成しました。

https://qiita.com/fsd-tetsu/items/8563b20518328fb93c80

プログラマの性でしょうか、結局、メッセージの送信・表示もできるチャット画面全体を作成しちゃいました。

ここでは、実装にあたってポイントとなった箇所を記載していきます。

# 最終成果物

[Svelte] の [REPL] で作成しました。

https://svelte.dev/repl/0f4b1028a5014fcaa9bb69641be2464a?version=3.58.0

見た目はこんな感じです。右側に今回作成したチャット画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2773745/15aa767f-d536-5ef2-3272-54b96067a1a3.png)
適当にメッセージを入力して遊んでみてください。

尚、AIの応答メッセージ（`dummy.json`）は [ChatGPT] :robot:に作成してもらいました。

# 実装時のポイント

1. [カスタムストア] を使って AI とのやり取りを隠蔽
1. [tick] を使って再描画後の値を確実に参照

## カスタムストアを使って AI とのやり取りを隠蔽

[カスタムストア] を使うことで AI とのやり取りを `store.js` ファイル内にまとめています。こうすることによって、コンポーネント側（～.svelteファイル）では AI とのやり取りの具体的な内容を意識することなく実装できました。

`App.svelte` では `store.js` から以下をインポートしています。

 - `messages` ストア
 - `isInquiring` ストア
 - `inquiry` 関数

```html:App.svelte
<script>
	import { createSession } from './store.js';
	import ChatArea from './ChatArea.svelte';

	const { messages, isInquiring, inquiry } = createSession();

	const handleSubmitMessage = (text) => inquiry("あなた", text);
</script>

<ChatArea	messages={$messages}
					handleSubmitMessage={handleSubmitMessage}
					isInquiring={$isInquiring}　/>
```

メッセージを送信するとどんなことが起きるかを簡単な図で表してみました。
```mermaid
sequenceDiagram
    participant c as App.svelte
    participant s as store.js
    participant is as isInquiring
    participant m as messages

    c->>s: inquiry("あなた", text)
    s->>is: trueに更新
    is->>c: 💥更新を通知
    c->>c: <ChatArea>を再描画
    loop
        s->>m: 更新
        m->>c:💥更新を通知
        c->>c:<ChatArea>を再描画
    end
    s->>is: falseに更新
    is->>c:💥更新を通知
    c->>c:<ChatArea>を再描画
```
実際には、`<ChatArea>` 全体が再描画されるわけではなく、更新が必要な箇所だけが再描画されます。

`App.svelte` で実装するときに意識するのは、`messages` と `isInquiring` がストアであることと、実際の値を参照するために `$` を付けること、ぐらいです。これだけでリアクティブな仕組みに対応できてしまいます。

こうなると、`store.js` 内で `dummy.json` を使っているのか、もしくは何等かの Web API を使っているのかの違いは、コンポーネントの実装に一切影響を与えません。`store.js` を修正するだけで本格的な（？）アプリケーションに移行できちゃいます。

## tick を使って再描画後の値を確実に参照

`ChatArea.svelte` では、メッセージが追加されると自動的に最下部までスクロールする仕様になっています。このスクロールをスムーズに見せるために [tick] を使っています。

[tick] は再描画が完了するのを待ってから処理を実行するための仕組みです。

説明するのがむずかしいので、[tick] を使う場合と使わない場合を動画で比較してみます。

[tick] を使う場合。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2773745/aba1404d-8a9c-fa84-fbd2-b9e75209bb9d.gif" alt="ChatArea_tick有.gif" width="300">

[tick] を使わない場合。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2773745/283ba671-e2f2-bf82-6f14-81be4104ed98.gif" alt="ChatArea_tick無.gif" width="300">

[tick] を使わない場合はスクロールが若干遅れていますね。[tick] を使うことでこの遅れを解消する効果があるわけです。

実装はこんな感じです。

```javascript:ChatArea.svelte
...
	$: {
		// 自動的に最下部までスクロールさせる
		messages;
		!!mainElement && tick().then(() => mainElement.scrollTo(0, mainElement.scrollHeight));
	}
...
```

:::note warn
`tick()` は `Promise` を返すので `await` できるのですが、`$:` ステートメント内だと `await` できませんでした。仕方なく `then()` で対応しました。
:::

# いけてない実装だと思うところ

`MessageInput.svelte` では、`text` 変数が変更されたら `resize()`　を実行して自動的に高さを変えるようにしています。
こんな感じです。

```javascript:MesageInput.svelte
...
	$: {
		text;
		resize();
	}
...
```

`text;` の行は普通なら「何の意味もない行があるじゃん」って思いますよね...。
ただ、この行が無いと `text` 変数が変更されたことを検知できないので、自動的に高さを変えることができなくなってしまうのです。（前述の `ChatArea.svelte` の自動スクロールの実装も同様に `messages;` という行があります。）

以下の様に強引に `if` 文を設ける方法がありますが、これだと空文字になったときに `resize()` が実行されないのです...。
```javascript:MesageInput.svelte
...
	$: {
		if (text) { // 😥 text が空文字の場合は false 扱いなので resize() は実行されない
			resize();
		}
	}
...
```

なんか、もやっとしますよね...
もっとうまい実装方法があったらぜひ教えてほしいです！:wave:

# REPL の Tips

[REPL] では「JS output」というタブをクリックすれば、コンポーネントがコンパイルされた後の JavaScript を確認できます。

例えば以下のように、実際には `$messages` という変数が定義されていて、`component_subscribe` 関数によって更新を検知したら `$messages` に `value` を再代入していることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2773745/b03967a0-4de9-cab8-dc77-d43b630130e5.png)
こういうデータを見せてくれるように設計されている点がこの [REPL] の良いところだと思います。

他にも「CSS output」で CSSを、「AST output」で AST（構文木）を確認できます。

# まとめ

[Svelte] を使って [ChatGPT] 風なチャット画面を作成できました。

[Svelte] の [カスタムストア] を使うことでコンポーネントを修正することなく挙動を変えられるように実装できました。また、[tick] を使うことでより丁寧な UI/UX を実現できることがわかりました。

[Svelte] はフロントエンド開発にとって便利な機能がシンプルに実現されていると思います。[Svelte] の開発者の方々の設計力はすばらしいですね。:smiley:

[Svelte]:https://svelte.dev/
[REPL]:https://svelte.dev/repl/
[ChatGPT]:https://openai.com/blog/chatgpt
[カスタムストア]:https://svelte.dev/tutorial/custom-stores
[tick]:https://svelte.dev/tutorial/tick