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

React カスタム例

この例では、Klleon Chat SDK の Web コンポーネントを使わずに、クライアントサイドで直接制御するカスタムチャットインターフェースの実装方法を示します。SDK イベントとメソッドを活用して、柔軟な UI 構成が可能です。

主な特徴

  • UI を自前で実装: SDK の基本コンポーネントを使わず React ベースで構築
  • リアルタイム状態反映: SDK イベントを通じて UI 自動更新
  • メソッドを直接呼び出し: 機能のテストや制御が明確に可能
  • 発話状態に応じた入力制御: UX 向上のための状態ベースのインタラクション

事前準備

SDK スクリプトの追加

public/index.html<head> セクションに以下のスクリプトを追加してください。
{VERSION} は実際の SDK バージョンに置き換えてください(例: 1.2.0)。

public/index.html
<script src="https://web.sdk.klleon.io/{VERSION}/klleon-chat.umd.js"></script>

カスタム React コンポーネント

CSS スタイル
custom-react-example.css
.custom-react-example-page {
display: flex;
width: 100%;
gap: 24px;
height: 720px;

.klleon-chat-container {
display: flex;
flex: 1;
flex-direction: column;
gap: 12px;

.avatar-container {
flex: 1;
}

.chat-container {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
border-radius: 24px;
border: 1px solid #ccc;
padding: 12px;
gap: 12px;
&::-webkit-scrollbar {
display: none;
}

.chat-item {
display: flex;
flex-direction: column;
gap: 12px;
background: grey;
color: #fff;
padding: 12px;
border-radius: 12px;
}
}
}

.control-container {
display: flex;
flex-direction: column;
flex: 1;
gap: 12px;

.log-container {
display: flex;
flex-direction: column;
gap: 12px;
}

.method-container {
display: flex;
flex-direction: column;
flex: 1;
gap: 12px;

input {
width: 320px;
}

.horizontal-control-item {
display: flex;
gap: 12px;
}

.vertical-control-item {
display: flex;
flex-direction: column;
gap: 12px;
}
}
}
}
src/App.tsx
import { ChatData, Status } from "@site/src/types/global";
import { useEffect, useRef, useState, CSSProperties } from "react";

// TODO: 実際の SDK キーとアバター ID に置き換えてください。
const SDK_KEY = "YOUR_SDK_KEY";
const AVATAR_ID = "YOUR_AVATAR_ID";

interface AvatarProps {
videoStyle?: CSSProperties;
volume?: number;
}

function App() {
const [isLoading, setIsLoading] = useState(false);
const [status, setStatus] = useState<Status>("IDLE");
const [chatData, setChatData] = useState<ChatData[]>([]);
const [message, setMessage] = useState("");
const [echoText, setEchoText] = useState("");
const [isChatStarted, setIsChatStarted] = useState(false);
const [isAvatarSpeaking, setIsAvatarSpeaking] = useState(false);
const [guideText, setGuideText] = useState(
"start chat ボタンで接続してください。"
);

const avatarContainerRef = useRef<HTMLElement & AvatarProps>(null);
const chatContainerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (avatarContainerRef.current) {
avatarContainerRef.current.videoStyle = {
borderRadius: "24px",
objectFit: "cover",
};
avatarContainerRef.current.volume = 100;
}
}, []);

useEffect(() => {
if (chatContainerRef.current) {
const observer = new MutationObserver(() => {
chatContainerRef.current?.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: "smooth",
});
});
observer.observe(chatContainerRef.current, {
childList: true,
subtree: true,
});
}
}, []);

const startChat = async () => {
setIsChatStarted(true);
const KlleonChat = window.KlleonChat;

KlleonChat.onStatusEvent((status: Status) => {
setStatus(status);
setIsLoading(status !== "VIDEO_CAN_PLAY");
});

KlleonChat.onChatEvent((chatData: ChatData) => {
setChatData((prev) => [...prev, chatData]);
if (chatData.chat_type === "PREPARING_RESPONSE") {
setIsAvatarSpeaking(true);
setGuideText("アバターが応答を準備中です。しばらくお待ちください。");
} else if (chatData.chat_type === "TEXT") {
setGuideText("アバターが発話中です。stopSpeech で停止できます。");
} else if (chatData.chat_type === "RESPONSE_IS_ENDED") {
setIsAvatarSpeaking(false);
setGuideText(
"アバターの発話が終了しました。メッセージを入力して会話を続けてください。"
);
}
});

try {
await KlleonChat.init({
sdk_key: SDK_KEY,
avatar_id: AVATAR_ID,
log_level: "debug",
});
setGuideText("接続が完了しました。");
} catch (error) {
console.error("SDK 初期化失敗:", error);
setIsLoading(false);
}
};

const handleEcho = () => {
window.KlleonChat.echo(echoText);
setEchoText("");
};

const handleMessageSend = () => {
window.KlleonChat.sendTextMessage(message);
setMessage("");
};

return (
<div className="base-react-example-page">
{!isChatStarted && (
<button
onClick={startChat}
disabled={isLoading}
className="start-chat-button"
>
チャット開始
</button>
)}
<div className="sdk-container">
{isLoading && (
<span className="loading-text">Klleon アバター読み込み中...</span>
)}
<avatar-container
ref={avatarContainerRef}
class="avatar-container" // className は使用不可、class 属性を使用
/>
<div className="chat-control-container">
<div className="chat-container" ref={chatContainerRef}>
{chatData.map((item) => (
<div className="chat-item" key={item.id}>
<h5>ChatType: {item.chat_type}</h5>
<h6>Message: {item.message}</h6>
</div>
))}
</div>
<div className="chat-echo-container">
<input
type="text"
value={echoText}
onChange={(e) => setEchoText(e.target.value)}
placeholder="echo 内容を入力..."
disabled={isLoading || isAvatarSpeaking}
className="echo-input"
/>
<button
onClick={handleEcho}
disabled={isLoading || isAvatarSpeaking}
className="echo-button"
>
echo 送信
</button>
</div>
<div className="chat-echo-container">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder={
isAvatarSpeaking
? "アバター発話中、終了後に入力してください。"
: "メッセージを入力..."
}
disabled={isAvatarSpeaking}
className="echo-input"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.nativeEvent.isComposing)
handleMessageSend();
}}
/>
<button
onClick={handleMessageSend}
disabled={isAvatarSpeaking}
className="echo-button"
>
メッセージ送信
</button>
</div>
</div>
<div className="chat-control-container">
<h5>ガイド: {guideText}</h5>
<h5>状態: {status}</h5>
</div>
</div>
</div>
);
}

export default App;

実行例

Guide: start chat ボタンを通じて接続してください
Status: IDLE