メインコンテンツまでスキップ

新年なのでLLMとのチャットアプリをイチから作ってみた①画面モックを作成

· 約11分
moritalous
お知らせ

過去にQiitaに投稿した内容のアーカイブです。

新年なので(?)、いちからLLMチャットアプリを作りたくなりました。

漠然とこんな条件で検討を始めました。

  • LLMはもちろん、Bedrockを使う
  • AWSのサーバーレスなサービスだけを使用する(固定費がほぼかからない構成)

いちからと言っても、いい感じのものがないか探したところ、Skeletonにチャットのコンポーネントが用意されていることを知りました。

https://www.skeleton.dev/

これを使ってチャレンジしました。

注記

SvelteもSvelteKitもTailwindもほぼ未経験デス。。

目次

全3回に分けて投稿しています。

プロジェクト作成

まずは、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(</>のアイコン)」を切り替えることができます。

  • 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>
  1. 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カラムのレイアウトができました。

  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>
  3. 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)のところを置換します。 (ちょっと画面が中途半端な感じですが、このあと修正していきますので続けます。

  4. Message Bubbles

    個々のメッセージの部分です。HostとGuestのやり取りなので、

    • Hostの場合:左側から発言
    • Guestの場合:右側から発言

    の見た目になるようになっています。

    1. Scriptタグの先頭に import { Avatar } from "@skeletonlabs/skeleton"; を追加
    2. <!-- Host Message Bubble --><pre>host: {JSON.stringify(bubble, null, 2)}</pre> の2行を消して、「Host Bubble Template」を貼り付ける
    3. <!-- Guest Message Bubble --><pre>guest: {JSON.stringify(bubble, null, 2)}</pre> の2行を消して、「Guest Bubble Template」を貼り付ける

    チャットっぽくなってきました。

  5. 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部分の違和感がなくなったと思います。(下の余白が気になりますが、後で修正します。)

    image.png

  6. Scroll to Bottom

    どんどん行きましょう。メッセージが追加されたときに、自動的に最新メッセージが画面内に表示されるようにスクロールする処理です。

    1. Scriptタグ内にlet elemChat: HTMLElement;を追加します。

    2. 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">
    3. Scriptタグ内にscrollChatBottom関数を追加します。

  7. 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>

    ここまでで、チャットっぽい入力までができました。

色々改善

すこし気になるところが出てきたので、改善しました。

  1. 左側ペインは使わなそうなので削除

    • ばっさり削除
    • 大本のdivタグからgrid grid-cols-[auto_1fr]のクラスを削除
  2. チャットの見た目(アイコン、色)を調整

    項目AI側ユーザー側備考
    アイコン🤖😀
    variant-glass-primaryvariant-glass-tertiaryVariants
    フォントサイズ 320320
  3. 初期表示をAI側だけの吹き出しに変更

    messageFeedの1つ目の項目を削除。あとはすこし値を変更

  4. 画面いっぱいに表示されるように調整

    • sectionタグにあるmax-h-[400px]を削除
    • 大本のdivタグにh-screenクラスを追加
    • 大本のdivタグの子のdivタグからsectionタグまでh-fullクラスを追加

    参考:Height

  5. プロンプト入力後、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}を追加

  6. プロンプト入力して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と接続していきます。


ソースコードの全体はこちらで確認可能です。

https://github.com/moritalous/skeleton-chat/

  • 該当のソースコード

https://github.com/moritalous/skeleton-chat/blob/main/src/routes/v1/%2Bpage.svelte