新年なので(?)、いちからLLMチャットアプリを作りたくなりました。
漠然とこんな条件で検討を始めました。
- LLMはもちろん、Bedrockを使う
- AWSのサーバーレスなサービスだけを使用する(固定費がほぼかからない構成)
いちからと言っても、いい感じのものがないか探したところ、Skeletonにチャットのコンポーネントが用意されていることを知りました。
これを使ってチャレンジしました。
SvelteもSvelteKitもTailwindもほぼ未経験デス。。
目次
全3回に分けて投稿しています。
- 新年なのでLLMとのチャットアプリをイチから作ってみた① ←このページ
- 新年なのでLLMとのチャットアプリをイチから作ってみた②
- 新年なのでLLMとのチャットアプリをイチから作ってみた③ (執筆中)
プロジェクト作成
まずは、Get Startedを参考にプロジェクトを作成します。
npm create skeleton-app@latest my-skeleton-app
ウィザード方式になっていますので回答していきます。
よくわからないので、最小構成っぽい形にしました。
┌ Create Skeleton App (version 0.0.54)
Welcome to Skeleton 💀! A UI toolkit for Svelte + Tailwind
Problems? Open an issue on https://github.com/skeletonlabs/skeleton/issues if none exists already.
│
◇ Which Skeleton app template?
│ Bare Bones
│
◇ Select a theme (top most selection will be default):
│ Skeleton
│
◇
What other packages would you like to install:
│ none
│
◇ Add type checking with TypeScript?
│ Yes, using TypeScript syntax
│
◇ What would you like setup in your project:
│ none
│
◇ Done installing
Done! You can now:
cd my-skeleton-app
npm install
npm run dev
Need some help or found an issue? Visit us on Discord https://discord.gg/EXqV7W8MtY
指示に従いnpm run dev
までやってみます。
cd my-skeleton-app
npm install
npm run dev
ブラウザでhttp://localhost:5173/
にアクセスしましょう。
表示されました。
VSCodeで作業を進める場合は、拡張機能「Svelte for VS Code」(svelte.svelte-vscode)をインストールすると、コードフォーマットなどができて便利です。
チャットの画面を構成する
プロジェクトの作成ができたので、こちらを参考に、チャット画面を作っていきます。
skeletonのドキュメントはよくできていて、「Preview(目のアイコン)」と「Code(>のアイコン)」を切り替えることができます。
Codeで表示されるものをコピペしながらチャット画面を作っていきましょう。
編集するソースはsrc/routes/+page.svelte
です。
以下のコードが書かれていますが、バッサリ全て消し去りましょう。
<!-- YOU CAN DELETE EVERYTHING IN THIS PAGE -->
<div class="container h-full mx-auto flex justify-center items-center">
<div class="space-y-5">
<h1 class="h1">Let's get cracking bones!</h1>
<p>Start by exploring:</p>
<ul>
<li><code class="code">/src/routes/+layout.svelte</code> - barebones layout</li>
<li><code class="code">/src/app.postcss</code> - app wide css</li>
<li>
<code class="code">/src/routes/+page.svelte</code> - this page, you can replace the contents
</li>
</ul>
</div>
</div>
-
Layout Columns
まずは、Layout Columnsのコードを持ってきます。
Two Column Layout
の方を選びました。src/routes/+page.svelte<div class="w-full grid grid-cols-[auto_1fr] gap-1"> <div class="bg-surface-500/30 p-4">(nav)</div> <div class="bg-surface-500/30 p-4">(feed)</div> </div>
devサーバーが起動していれば、ソースを保存すると自動で反映されます。
2カラムのレイアウトができました。
-
Layout Rows
続いてLayout Rowsです。Three Row Layoutは左側のカラムの中に、Two Row Layoutを右側のカラムの中にセットします。
左側は(nav)を置換、右側は(feed)を置換します。
<div class="w-full grid grid-cols-[auto_1fr] gap-1"> - <div class="bg-surface-500/30 p-4">(nav)</div> + <div class="bg-surface-500/30 p-4"> + <div class="h-full grid grid-rows-[auto_1fr_auto] gap-1"> + <div class="bg-surface-500/30 p-4">(search)</div> + <div class="bg-surface-500/30 p-4">(list)</div> + <div class="bg-surface-500/30 p-4">(footer)</div> + </div> + </div> - <div class="bg-surface-500/30 p-4"> + <div class="h-full grid grid-rows-[1fr_auto] gap-1"> + <div class="bg-surface-500/30 p-4 overflow-y-auto">(feed)</div> + <div class="bg-surface-500/30 p-4">(prompt)</div> + </div> + </div> </div>
-
Message Feed
Message FeedはTypeScriptとHTMLで構成されています。
TypeScriptコードは、
src/routes/+page.svelte
の先頭にscriptタグを追加し、その中に記述します。<script lang="ts"> let messageFeed = [ { id: 0, host: true, avatar: 48, name: "Jane", timestamp: "Yesterday @ 2:30pm", message: "Some message text.", color: "variant-soft-primary", }, { id: 1, host: false, avatar: 14, name: "Michael", timestamp: "Yesterday @ 2:45pm", message: "Some message text.", color: "variant-soft-primary", }, ]; </script> (このあと<dvv>タグが続きます)
HTMLの部分は
(feed)
のところを置換します。
(ちょっと画面が中途半端な感じですが、このあと修正していきますので続けます。 -
Message Bubbles
個々のメッセージの部分です。HostとGuestのやり取りなので、
- Hostの場合:左側から発言
- Guestの場合:右側から発言
の見た目になるようになっています。
- Scriptタグの先頭に
import { Avatar } from "@skeletonlabs/skeleton";
を追加 -
<!-- Host Message Bubble -->
、
<pre>host: {JSON.stringify(bubble, null, 2)}</pre>
の2行を消して、「Host Bubble Template」を貼り付ける -
<!-- Guest Message Bubble -->
、
<pre>guest: {JSON.stringify(bubble, null, 2)}</pre>
の2行を消して、「Guest Bubble Template」を貼り付ける
チャットっぽくなってきました。
-
Prompt
チャットのテキストを入力するPrompt欄です。
TypeScriptのコードをScriptタグ内の末尾に、HTMLのコードを(prompt)に置換します。
あれ?若干ダサめですね。。
Formsのページを確認すると、Forms用のプラグインを導入する必要があるようです。
一度devサーバーを
Ctrl+c
で停止し、以下のコマンドを実行します。npm install -D @tailwindcss/forms
my-skeleton-app/tailwind.config.ts
に以下の設定を追加します。import { join } from 'path' import type { Config } from 'tailwindcss' import { skeleton } from '@skeletonlabs/tw-plugin' + import forms from '@tailwindcss/forms'; export default { darkMode: 'class', content: ['./src/**/*.{html,js,svelte,ts}', join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')], theme: { extend: {}, }, plugins: [ skeleton({ themes: { preset: [ { name: 'skeleton', enhancements: true, }, ], }, }), + forms, ], } satisfies Config;
npm run dev
でdevサーバーを起動して画面を確認するとPrompt部分の違和感がなくなったと思います。(下の余白が気になりますが、後で修正します。) -
Scroll to Bottom
どんどん行きましょう。メッセージが追加されたときに、自動的に最新メッセージが画面内に表示されるようにスクロールする処理です。
-
Scriptタグ内に
let elemChat: HTMLElement;
を追加します。 -
bind:this={elemChat} class="overflow-y-auto
を39行目辺りにあるsectionタグに追加します。- <section class="w-full max-h-[400px] p-4 overflow-y-auto space-y-4"> + <section bind:this={elemChat} class="w-full max-h-[400px] p-4 overflow-y-auto space-y-4">
-
Scriptタグ内にscrollChatBottom関数を追加します。
-
-
Add a Message
メッセージを追加する処理です。
Scriptタグ内にaddMessage関数を追加しますが、ドキュメントのままだとエラーになります。
Page Sourceから該当の部分(
getCurrentTimestamp()
とaddMessage()
)を持ってきます。function getCurrentTimestamp(): string { return new Date().toLocaleString("en-US", { hour: "numeric", minute: "numeric", hour12: true, }); } function addMessage(): void { const newMessage = { id: messageFeed.length, host: true, avatar: 48, name: "Jane", timestamp: `Today @ ${getCurrentTimestamp()}`, message: currentMessage, color: "variant-soft-primary", }; // Update the message feed messageFeed = [...messageFeed, newMessage]; // Clear prompt currentMessage = ""; // Smooth scroll to bottom // Timeout prevents race condition setTimeout(() => { scrollChatBottom("smooth"); }, 0); }
SendボタンのクリックイベントでaddMessage関数を呼び出します。
- button class="variant-filled-primary">Send</button> + button class="variant-filled-primary" on:click={addMessage}>Send</button>
ここまでで、チャットっぽい入力までができました。
色々改善
すこし気になるところが出てきたので、改善しました。
-
左側ペインは使わなそうなので削除
- ばっさり削除
- 大本のdivタグから
grid grid-cols-[auto_1fr]
のクラスを削除
-
チャットの見た目(アイコン、色)を調整
項目 AI側 ユーザー側 備考 アイコン 🤖 😀 色 variant-glass-primary variant-glass-tertiary Variants フォントサイズ 320 320 -
初期表示をAI側だけの吹き出しに変更
messageFeed
の1つ目の項目を削除。あとはすこし値を変更 -
画面いっぱいに表示されるように調整
- sectionタグにある
max-h-[400px]
を削除 - 大本のdivタグに
h-screen
クラスを追加 - 大本のdivタグの子のdivタグからsectionタグまで
h-full
クラスを追加
参考:Height
- sectionタグにある
-
プロンプト入力後、
Ctrl+Enter
でプロンプト入力を確定させられるように調整Enterのみで確定させたかったけど、日本語変換時のEnter入力との競合が発生したので、Ctrlとの同時押しにしました。
-
onPromptKeydown関数を追加
function onPromptKeydown(event: KeyboardEvent): void { if (event.ctrlKey && ["Enter"].includes(event.code)) { event.preventDefault(); addMessage(); } }
-
textareaタグに
on:keydown={onPromptKeydown}
を追加
-
-
プロンプト入力して1秒後に、AI側がオウム返しするように修正
-
addAiMessage関数を追加
function addAiMessage(userMessage: string): void { const newMessage = { id: messageFeed.length, host: false, name: "Claude", timestamp: `Today @ ${getCurrentTimestamp()}`, message: `あなたは「${userMessage}」といいました。`, color: "variant-glass-primary", }; // Update the message feed messageFeed = [...messageFeed, newMessage]; // Smooth scroll to bottom // Timeout prevents race condition setTimeout(() => { scrollChatBottom("smooth"); }, 0); }
-
addMessage関数の中でaddAiMessage関数を呼び出す
const tmpMessage = currentMessage; setTimeout(() => { addAiMessage(tmpMessage); }, 1000);
-
いい感じになってきました😁😁😁
長くなりそうなので、今回はここまでです。次回はAWSと接続していきます。
ソースコードの全体はこちらで確認可能です。
- 該当のソースコード