@airsurfer09/web-handsfree
Version:
A React package for integrating Convai's AI-powered voice assistants with LiveKit for real-time audio/video conversations
184 lines • 12.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useState, useRef, useEffect } from "react";
import { RoomContext, RoomAudioRenderer, } from "@livekit/components-react";
import { MicrophoneIcon } from "./MicrophoneIcon";
/**
* ChatBot component for real-time conversation with Convai AI assistants.
*
* Provides a complete chat interface with message history, typing indicators,
* connection status, and audio controls. Supports both text and voice interactions.
*
* @param {ChatBotProps} props - Component props
* @param {ConvaiClient} props.convaiClient - Convai client instance with activity status
* @param {Function} props.onConnect - Callback function to handle connection
*
* @example
* ```tsx
* function App() {
* const convaiClient = useConvaiClient();
*
* const handleConnect = async () => {
* await convaiClient.connect({
* apiKey: 'your-api-key',
* characterId: 'your-character-id'
* });
* };
*
* return (
* <ChatBot
* convaiClient={convaiClient}
* onConnect={handleConnect}
* />
* );
* }
* ```
*/
export const ChatBot = ({ convaiClient, onConnect, }) => {
const [isMinimized, setIsMinimized] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [inputText, setInputText] = useState("");
const [isSending, setIsSending] = useState(false);
const messagesEndRef = useRef(null);
const chatContainerRef = useRef(null);
const inputRef = useRef(null);
const { state, activity, chatMessages, sendUserTextMessage, audioControls } = convaiClient;
// Filter messages to only show the specified types in chat UI
const filteredChatMessages = chatMessages.filter((message) => message.type === "user-llm-text" ||
message.type === "bot-llm-text" ||
message.type === "bot-emotion");
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({
behavior: "smooth",
block: "end",
});
}
}, [filteredChatMessages]);
// Simulate typing indicator for Convai messages
useEffect(() => {
if (filteredChatMessages.length > 0) {
const lastMessage = filteredChatMessages[filteredChatMessages.length - 1];
if (lastMessage.type === "bot-llm-text") {
setIsTyping(true);
const timer = setTimeout(() => setIsTyping(false), 1000);
return () => clearTimeout(timer);
}
}
}, [filteredChatMessages]);
const getStatusColor = () => {
if (state.isConnected)
return "bg-emerald-400 shadow-emerald-400/50";
if (state.isConnecting)
return "bg-yellow-400 shadow-yellow-400/50";
return "bg-slate-400 shadow-slate-400/50";
};
const getStatusText = () => {
if (state.isConnected)
return "Connected";
if (state.isConnecting)
return "Connecting...";
return "Disconnected";
};
const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
};
const handleSendMessage = async () => {
if (!inputText.trim() || isSending || !state.isConnected)
return;
setIsSending(true);
try {
console.log("MESSAGE", inputText);
sendUserTextMessage(inputText);
setInputText("");
}
catch (error) {
console.error("Failed to send message:", error);
}
finally {
setIsSending(false);
}
};
const handleKeyPress = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
const renderMessage = (message, index) => {
const isLastMessage = index === filteredChatMessages.length - 1;
switch (message.type) {
case "user-llm-text":
return (_jsx("div", { style: {
padding: "8px 0",
}, children: _jsxs("div", { className: "items-end", style: {
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
maxWidth: "100%",
}, children: [_jsx("div", { className: "transparent border border-emerald-500/30 rounded-2xl px-6 py-4 shadow-lg backdrop-blur-sm", style: { padding: "12px 16px" }, children: _jsx("p", { className: "text-emerald-100 text-sm leading-relaxed", children: message.content }) }), _jsx("div", { className: "flex items-center mt-2 space-x-2", style: {
marginRight: "12px",
marginTop: "4px",
}, children: _jsxs("span", { className: "text-emerald-400/60 text-xs font-medium", children: ["You ", formatTime(message.timestamp)] }) })] }) }, message.id));
case "bot-llm-text":
return (_jsx("div", { style: {
padding: "8px 0",
}, children: _jsxs("div", { className: "items-start", style: {
display: "flex",
flexDirection: "column",
maxWidth: "85%",
}, children: [_jsx("div", { className: "bg-emerald-500/20 border border-emerald-500/30 rounded-2xl px-6 py-4 shadow-lg backdrop-blur-sm", style: { padding: "12px 16px" }, children: _jsx("p", { className: "text-slate-100 text-sm leading-relaxed", children: message.content }) }), _jsx("div", { className: "flex items-center mt-2 space-x-2", style: {
marginLeft: "12px",
marginTop: "4px",
}, children: _jsxs("span", { className: "text-slate-400/60 text-xs font-medium", children: ["Convai ", formatTime(message.timestamp)] }) })] }) }, message.id));
case "bot-emotion":
return (_jsx("div", { className: "flex justify-center mb-4 animate-in fade-in duration-500", children: _jsx("div", { className: "bg-gradient-to-r from-purple-500/20 to-pink-500/20 border border-purple-500/30 rounded-full px-4 py-2 shadow-lg backdrop-blur-sm", children: _jsxs("p", { className: "text-purple-200 text-sm font-medium", children: ["\uD83D\uDE0A ", message.content] }) }) }, message.id));
// Legacy message types - these won't be shown in filtered view but handle them gracefully
case "user":
case "convai":
case "emotion":
case "behavior-tree":
case "action":
return null;
default:
return null;
}
};
// Auto-open when connected
useEffect(() => {
if (state.isConnected && isMinimized) {
setIsMinimized(false);
}
}, [state.isConnected, isMinimized]);
return (_jsx(RoomContext.Provider, { value: convaiClient.room, children: _jsxs("div", { className: "relative rounded-4xl glass", style: { width: "40vw", maxWidth: "550px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }, children: [_jsx("div", { className: "rounded-t-2xl flex items-center justify-around" }), _jsx("div", { className: `rounded-b-2xl overflow-hidden transition-all duration-300 ${isMinimized ? "w-80 h-16" : "w-96 h-[500px]"}`, style: {
minHeight: 190,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
}, children: !isMinimized && (_jsxs(_Fragment, { children: [_jsxs("div", { ref: chatContainerRef, className: "flex-1 overflow-y-auto p-4 space-y-2 bg-slate-900/20 h-[320px] scrollbar-thin scrollbar-thumb-emerald-500/30 scrollbar-track-transparent", style: {
maxHeight: "500px",
overflowY: "scroll",
}, children: [chatMessages.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-full", children: _jsxs("div", { className: "text-center text-slate-400", style: {
paddingTop: "20px",
}, children: [_jsx("p", { className: "text-sm font-semibold text-slate-300", children: "Ready to Chat" }), _jsx("p", { className: "text-xs opacity-75 max-w-48", children: "Click connect to start your conversation with the AI assistant" })] }) })) : (_jsx("div", { style: {
height: "400px !important",
overflowY: "scroll",
}, children: filteredChatMessages.map((message, index) => renderMessage(message, index)) })), _jsx("div", { ref: messagesEndRef })] }), _jsx("div", { className: "p-4 border-emerald-500/20 bg-slate-900/30", children: !state.isConnected ? (_jsx("button", { onClick: onConnect, disabled: state.isConnecting, className: "w-full px-4 py-3 bg-gradient-to-r from-emerald-500 to-emerald-600 hover:from-emerald-600 hover:to-emerald-700 disabled:from-emerald-500/50 disabled:to-emerald-600/50 text-white rounded-xl font-semibold transition-all duration-200 shadow-lg hover:shadow-emerald-500/25 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:ring-offset-2 focus:ring-offset-slate-900 btn-hover-lift", children: state.isConnecting ? (_jsxs("div", { className: "flex items-center justify-center space-x-2", children: [_jsx("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" }), _jsx("span", { children: "Connecting..." })] })) : (_jsx("div", { className: "flex items-center justify-center space-x-2", children: "Connect To Convai" })) })) : (_jsxs("div", { style: {
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "8px",
}, children: [_jsx("button", { onClick: () => audioControls.toggleAudio(), className: `px-3 py-3 rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-900 text-white ${audioControls.isAudioMuted
? "bg-red-500/20 border border-red-500/30 hover:bg-red-500/30"
: "bg-emerald-500/20 border border-emerald-500/30 hover:bg-emerald-500/30"}`, title: audioControls.isAudioMuted ? "Unmute Microphone" : "Mute Microphone", children: _jsx(MicrophoneIcon, { width: 24, height: 24 }) }), _jsx("input", { ref: inputRef, type: "text", value: inputText, onChange: (e) => setInputText(e.target.value), onKeyPress: handleKeyPress, placeholder: "Type your message...", disabled: isSending, className: "px-4 py-3 rounded-lg flex-1", style: {
color: "black",
outline: "none",
flex: 1,
} }), _jsx("button", { onClick: handleSendMessage, disabled: !inputText.trim() || isSending, className: "px-4 py-3 bg-gradient-to-r from-emerald-500 to-emerald-600 hover:from-emerald-600 hover:to-emerald-700 disabled:from-emerald-500/30 disabled:to-emerald-600/30 text-white rounded-lg font-semibold transition-all duration-200 shadow-lg hover:shadow-emerald-500/25 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:ring-offset-2 focus:ring-offset-slate-900 btn-hover-lift disabled:cursor-not-allowed", children: isSending ? (_jsx("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-lg animate-spin" })) : (_jsx(_Fragment, { children: "Send" })) })] })) })] })) }), _jsx(RoomAudioRenderer, {})] }) }));
};
//# sourceMappingURL=ChatBot.js.map