【React+Chat-ui-kit】LLM時代の手作りチャット画面の作成手順
概要
LLMの登場により, アプリケーション面ではチャット画面を実装したいという要望はあると思う.
本記事では, お手軽にブラウザ上で動くチャット画面の作成ライブラリchat-ui-kit
を紹介する.
React
の上で動くため, 軽量かつシンプルな実装ができることが魅力だと思う.
技術構成
-
ブラウザ・チャット画面にはjavascript(node.js)を使用する
-
主な技術構成は以下の通り
- ブラウザの画面: NextJS (Reactの派生)
- +型定義: typescript
- デザイン: bootstrap
- チャット画面: chat-ui-kit-react
-
MacBookPro/OS: Sequioa 15.5
準備
node.js
関連や, チャット画面を作成するchat-ui-kit-react
をインストールする
本記事では, Ollama
を用いた対話型エージェントも実装するが, その導入部分については以前の記事で説明しているためここでは省く.
next.js
Next.js
は, ReactベースのJavaScriptフレームワークです.
React
とは何が違うの?って方は以下の記事を参考にして下さい😇
好きなディレクトリを作成し, 以下のコマンドを作成する.
mkdir samlplechat
cd samplechat
npx create-next-app@latest # これだけ
このコマンドのあと、画面上にいろいろなオプションが出てくるので以下のように答える
✔ What is your project named? … my-app
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … No
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No
Creating a new Next.js app in ...
I番目の質問は, アプリ(のファイル)名, 2番目の質問は, typescript.js
の使用の有無...
以下の記事も参考にしてください.
chat-ui-kit
chat-ui-kit
は, Web チャットアプリ開発向けのオープンソース UI ツールキットで, react
との相性が良い.
install
インストールするには以下のコマンドをターミナルでうつ.
npm install @chatscope/chat-ui-kit-react
npm install @chatscope/chat-ui-kit-styles
基本要素
import styles from "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import {
MainContainer,
ChatContainer,
MessageList,
Message,
MessageInput,
} from "@chatscope/chat-ui-kit-react";
<div style={{ position: "relative", height: "500px" }}>
<MainContainer>
<ChatContainer>
<MessageList>
<Message
model={{
message: "Hello my friend",
sentTime: "just now",
sender: "Joe",
}}
/>
</MessageList>
<MessageInput placeholder="Type message here" />
</ChatContainer>
</MainContainer>
</div>;
chat-ui-kit
の説明をした日本語記事はまだまだ少ない. が,最低限の要素について説明しておく.
<MessageList>
内がチャットスペースのメッセージ置き場となる.
そして,<Message>
がそのメッセージそのものとなる. つまり, <Message>
要素が5個含まれていれば, 画面上にはメッセージが5個表示されることとなる.
さて, <Message>
要素には, 各メッセージを識別するkey
と, メッセージのデザイン・内容を決めるmodel
がある. また, <Message>
要素のさらに内側にアバターアイコンである<Avatar>
を含めることができる.
つぎに, <MessageList>
要素の下に配置する<MessageInput>
は, 問い合わせ入力枠を作成する. <MessageInput>
要素には, 送信ボタンを押した後の処理onSend
や, 送信ボタンを押せるかどうかのsendDisabled
がある.
後者は, 何か送信処理中は送信ボタンを押せないようにするなどの制御に使える.
具体例をもう少し知りたい場合は以下の記事を参考にしてください.
残り
デザインにbootstrap
, LLMからの応答文を整形するためにmarked
を導入しておく.
npm install bootstrap # デザイン系
npm install marked # Markdownをhtmlに変換する
LLMとチャットしてみる
道具が揃ったので, チャット機能をUIから作っていこう.
全体の構成として,以下を想定する.
- チャットの画面はブラウザ(
http://localhost:3000/
) - LLMやブラウザとのやりとりはPythonサーバー(
http://localhost:8080/
) - LLMは
Ollama
のサーバーを介して行う(http://localhost:11434/
)
またファイル階層は以下の通りとする.
/my-app
├── /ChatAgent
│ ├── main.py # サーバー起動/ブラウザとの通信
│ └── agent.py # LLMエージェント
├── /src
│ ├── /app # 基本ページ
│ │ └── page.tsx
│ ├── /components #
│ │ └── Chat.tsx
│ ├── /context # 状態変数の置き場
│ └── GroupContext.py
└── /public # アイコン画像置き場
コード例
import os
import json
import warnings
import argparse
from http.server import BaseHTTPRequestHandler, HTTPServer
from Agent import Agent
warnings.simplefilter('ignore')
os.environ['HTTP_PROXY'] = ''
os.environ['HTTPS_PROXY'] = ''
agent = None # 本サーバーで稼働するエージェント
def Conversation(agent, requestText):
return agent.conversation(requestText)
class MyHandler(BaseHTTPRequestHandler):
def do_POST(self): # POST リクエストを受けた場合
global agent
print(self.headers)
content_len=int(self.headers.get('content-length'))
requestBody = self.rfile.read(content_len).decode('utf-8')
# サーバー側のコマンドプロンプト出てくる表記
print('requestBody=' + requestBody)
jsonData = json.loads(requestBody)
print('**JSON**')
errors = [] # 処理中のエラーを列挙
data = jsonData["data"]
action = data["action"]
if action:
if action == "chat":
# 会話したい場合
requestText = data["query"]
response = Conversation(agent, requestText)
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.end_headers()
responseBody = json.dumps({"status": 200,
"result": response})
self.wfile.write(responseBody.encode("utf-8"))
def importargs():
parser = argparse.ArgumentParser("これは練習用サーバー")
parser.add_argument("--host", "-H", required=False, default="localhost")
parser.add_argument("--port", "-P", required=False, type=int, default=8000)
args = parser.parse_args()
return args.host, args.port
def run(server_class = HTTPServer, handler_class = MyHandler, server_name = "localhost", port=8000):
server = server_class((server_name, port), handler_class)
print("サーバー稼働中")
server.serve_forever()
def main():
global agent
# サーバーを立ててデータの受信を待つ
host, port = importargs() # サーバー情報の取得
agent = Agent()
run(server_name = host, port = port) # サーバーを起動
if __name__ == "__main__":
main()
import os
os.environ['HTTP_PROXY'] = ''
os.environ['HTTPS_PROXY'] = ''
from langchain_community.llms import Ollama
class Agent:
def __init__(self):
# ローカルLLMを搭載する
self.llm = Ollama(
base_url="http://localhost:11434",
model="elyza:jp8b") # 作成したモデル名を指定
def conversation(self, text):
return self.llm.invoke(text)
'use client' // 強制的にクライアントレンダーへ
// import dynamic from "next/dynamic";
import 'bootstrap/dist/css/bootstrap.min.css'; // BootstrapのCSSをインポート
import { GroupsProvider } from '@/context/GroupsContext';
import Chat from "../components/Chat";
export default function Home() {
return (
<GroupsProvider>
{/* ヘッダー */}
<div className="d-flex flex-column" style={{width: "100%"}}>
<header className="bg-white text-center py-2 mt-1">
<h5>{"bot"}</h5>
</header>
</div>
<div id="home" className="container-fluid vh-100 d-flex justify-content-center"
style={{ backgroundColor: '#f8f9fa', overflowY: "hidden"}}>
<div className="d-flex justify-content-center" style={{ width: '40%', padding: '1rem', borderLeft: '1px solid #ddd',borderRight: '1px solid #ddd' }}>
<div className="bg-light p-3" style={{width: "100%", height: "95%"}}>
<div className="shadow p-2 mb-1 rounded bg-secondary text-white ">
チャットスペース</div>
<Chat />
</div>
</div>
</div>
</GroupsProvider>
);
}
import React, { useState } from "react";
import { useRoadGroups } from '@/context/GroupsContext';
import { MainContainer, ChatContainer, MessageList,
Message, MessageInput, Avatar, TypingIndicator, Button } from "@chatscope/chat-ui-kit-react";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.css"
import { Marked } from "marked";
const initialMessages = {message: "質問をしてください", sender: "Agent", direction: "incoming"}
const AvaterIcon = (avaterName: string) => {
if (avaterName === "user") {
return "自分のアイコン.png"
} else if (avaterName === "Agent") {
return "https://chatscope.io/storybook/react/assets/zoe-E7ZdmXF0.svg"
}
}
const marked = new Marked(
{
gfm: true,
breaks: true,
}
)
const Chat = () => {
const [messages, setMessages] = useState([initialMessages]);
const {isLoading, setIsLoading, isReady, setIsReady} = useRoadGroups();
setIsReady(true)
setIsLoading(false)
const sendQuery = (text: string, action: string) => {
let message = {message: text, sender: "user", direction: "outgoing", type: "text"};
setMessages((prev) => [...prev, message]);
// LLMへ問い合わせ
const jsonData = {
"data":{
"action" : action,
"query": text
}
}
const data = JSON.stringify(jsonData)
const options = {
method: 'POST',
body: data,
};
console.log(data)
setIsLoading(true)
fetch("http://localhost:8000/", options)
.then(response => {
if (!response.ok){
return Promise.reject(new Error('エラーです'));
}
return response.json()
})
.then(data => {
// 処理成功の場
console.log(data)
let result = data["result"]
message = {message: marked.parse(result), sender: "Agent", direction: "incoming", type: "html"};
setMessages((prev) => [...prev, message]);
setIsLoading(false)
})
.catch(error => {
// エラーを受けた場合
console.error('There was a problem with the fetch operation:', error);
message = {message: "もう一度送信してください...", sender: "Agent", direction: "incoming", type: "text"};
setMessages((prev) => [...prev, message]);
setIsLoading(false)
alert(error)
});
}
return (
<div style={{ position: "relative", height: "95%", fontSize: "1em"}}>
<MainContainer>
<ChatContainer>
<MessageList typingIndicator={isLoading ? <TypingIndicator content="問い合わせ中..." /> : null}>
{messages.map((m, mi) =>
(<Message
key={mi}
model={{
message: m.message,
sentTime: "just now",
sender: m.sender,
position: "normal",
direction: m.direction,
type: m.type
}}
>
<Avatar
name= {m.sender}
src= {AvaterIcon(m.sender)}
/>
</Message>
)
)}
</MessageList>
<MessageInput
placeholder="問い合わせを記入してください..."
onSend = {(innerHtml, textContent, message) => sendQuery(message, "chat")}
sendDisabled={!isReady || isLoading} />
</ChatContainer>
</MainContainer>
</div>
)
};
export default Chat;
import React, { createContext, useContext, useState } from "react";
type ContextProps = {
isLoading: boolean;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
isReady: boolean;
setIsReady: React.Dispatch<React.SetStateAction<boolean>>;
};
const GroupsContext = createContext<ContextProps | undefined>(undefined);
export const useRoadGroups = () => {
const context = useContext(GroupsContext);
if (!context) throw new Error("useRoadGroups must be within a Provider");
return context;
};
export const GroupsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isReady, setIsReady] = useState<boolean>(false);
return (
<GroupsContext.Provider value = {{isLoading, setIsLoading, isReady, setIsReady}}>
{children}
</GroupsContext.Provider>
)
}
上記の画面が作成できれば成功. チャット画面を作成することができるのでぜひトライしてみよう.
まとめ
本記事では, お手軽にブラウザ上で動くチャット画面の作成ライブラリchat-ui-kit
を紹介した.
以下に, ChatGPTに聞いた他の実装方法も示しておく. 参考になれば幸いである.
目的 | おすすめ |
---|---|
軽量なチャットUIを自作したい | ✅ react-chat-ui / react-chat-elements
|
チャットボット的な対話UIが欲しい | ✅ BotUI
|
本格的なチャットをすぐ使いたい | ✅ Stream Chat
|
完全自作+リアルタイム処理も自分で | ✅ socket.io + 任意のUI |
比較説明
🧱 ① UIを素早く作れるチャット用UIライブラリ・コンポーネント
✅ react-chat-ui
GitHub: https://github.com/brandonmowat/react-chat-ui
特徴:
Reactベースでチャットバブル・送受信・リストなどが揃っている
シンプルなAPIとUI構成
向いている用途: 「自分でバックエンドを作る or 外部APIと連携したチャットUI」
npm install react-chat-ui
✅ react-chat-elements
GitHub: https://github.com/Detaysoft/react-chat-elements
特徴:
よりリッチなUI(日時表示・ユーザーアバター・メッセージタイプ別など)
チャットアプリらしいデザインが標準で組み込まれている
UI例: メッセージ、リスト、送信ボタン、画像・動画・音声対応
npm install react-chat-elements
✅ BotUI
URL: https://botui.org/
特徴:
チャットボット風のUIを簡単に作れる
非React系。CDN読み込みでもOK
向いている用途: 「ステップ形式のチャット」「FAQ形式の会話ボット」
<script src="https://cdn.jsdelivr.net/npm/botui/build/botui.min.js"></script>
🛠 ② UI+リアルタイム通信込み(バックエンド連携前提)
✅ Stream Chat UI Kit
URL: https://getstream.io/chat/
特徴:
完全なチャットUI+リアルタイムAPI(無料枠あり)
React用UIキットが非常に充実
ビジネス用チャット、サポートチャットなどにも強い
使い方: Reactコンポーネントを並べてAPIキーと接続すれば動作
npm install stream-chat stream-chat-react
✅ socket.io + 自作UI
URL: https://socket.io/
特徴:
WebSocketベースでリアルタイム通信を実装
UIは自作(or 上記のUIライブラリと組み合わせ)
向いている用途: 学習目的・完全自作したい場合
🧩 ③ Tailwindやshadcn/uiを使ってチャットUIを自作する場合
もしあなたが Next.js + Tailwind + shadcn/ui のような構成で開発している場合、以下の方法もおすすめです:
TailwindのFlex/Gap構成でバブルUIを作成
shadcn/ui の Input や Card コンポーネントで簡易チャットUI構成
メッセージは overflow-y-scroll + max-h を使えばスクロール式に
参考サイト
本記事で扱った
chat-ui-kit
以外の選択肢にも検討しているので参考にできるかも
Python
の標準ライブラリであるhttp.server
の使用方法
Discussion