-
-
Notifications
You must be signed in to change notification settings - Fork 1
Frontend Integration
Guide to integrating chat UI components with modern JavaScript frameworks.
- Overview
- React Components
- Vue Components
- Angular Components
- Vanilla JavaScript
- TypeScript Support
- Styling and Themes
- Custom Components
PHP Chatbot provides flexible frontend integration options for modern web applications. Choose from pre-built components for popular frameworks or create your own custom implementation.
| Framework | Component Type | TypeScript | Styling | Best For |
|---|---|---|---|---|
| React | Functional + Hooks | ✅ | CSS Modules, Styled Components | Modern React apps |
| Vue | Composition API | ✅ | Scoped CSS, CSS-in-JS | Vue 3 applications |
| Angular | Standalone Components | ✅ | Angular Material, SCSS | Enterprise applications |
| Vanilla JS | ES6 Classes | ✅ | CSS, SCSS | Any web application |
- Copy component files to your project
- Install dependencies (if any)
- Configure API endpoint
- Customize styling as needed
- Initialize component in your app
Create components/Chatbot.jsx:
import React, { useState, useRef, useEffect } from 'react';
import './Chatbot.css';
const Chatbot = ({
apiUrl = '/api/chatbot/message',
title = 'Chat Support',
greeting = 'Hello! How can I help you?',
placeholder = 'Type your message...',
position = 'bottom-right'
}) => {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState([
{ id: 1, text: greeting, sender: 'bot', timestamp: Date.now() }
]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [conversationId] = useState(() =>
`conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
);
const messagesEndRef = useRef(null);
const inputRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
const sendMessage = async () => {
if (!inputValue.trim() || isLoading) return;
const userMessage = {
id: Date.now(),
text: inputValue,
sender: 'user',
timestamp: Date.now()
};
setMessages(prev => [...prev, userMessage]);
setInputValue('');
setIsLoading(true);
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content,
},
body: JSON.stringify({
message: userMessage.text,
conversation_id: conversationId,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const botMessage = {
id: Date.now() + 1,
text: data.reply || 'Sorry, I encountered an error.',
sender: 'bot',
timestamp: Date.now()
};
setMessages(prev => [...prev, botMessage]);
} catch (error) {
console.error('Chatbot error:', error);
const errorMessage = {
id: Date.now() + 1,
text: 'Sorry, I\'m having trouble connecting. Please try again.',
sender: 'bot',
timestamp: Date.now()
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
const toggleChat = () => {
setIsOpen(!isOpen);
};
return (
<div className={`chatbot-container chatbot-${position}`}>
{isOpen && (
<div className="chatbot-popup">
<div className="chatbot-header">
<h4>{title}</h4>
<button
className="chatbot-close"
onClick={toggleChat}
aria-label="Close chat"
>
×
</button>
</div>
<div className="chatbot-messages">
{messages.map((message) => (
<div
key={message.id}
className={`chatbot-message ${message.sender}-message`}
>
<div className="message-content">
{message.text}
</div>
<div className="message-time">
{new Date(message.timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
))}
{isLoading && (
<div className="chatbot-message bot-message">
<div className="message-content">
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className="chatbot-input">
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={placeholder}
disabled={isLoading}
maxLength={2000}
/>
<button
onClick={sendMessage}
disabled={!inputValue.trim() || isLoading}
className="send-button"
aria-label="Send message"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
)}
<button
className="chatbot-toggle"
onClick={toggleChat}
aria-label={isOpen ? "Close chat" : "Open chat"}
>
{isOpen ? '×' : '💬'}
</button>
</div>
);
};
export default Chatbot;Create components/Chatbot.tsx:
import React, { useState, useRef, useEffect, useCallback } from 'react';
import './Chatbot.css';
interface Message {
id: number;
text: string;
sender: 'user' | 'bot';
timestamp: number;
}
interface ChatbotProps {
apiUrl?: string;
title?: string;
greeting?: string;
placeholder?: string;
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
theme?: 'light' | 'dark';
maxMessages?: number;
onMessageSent?: (message: string) => void;
onMessageReceived?: (message: string) => void;
}
interface ApiResponse {
reply: string;
conversation_id?: string;
timestamp?: string;
error?: string;
}
const Chatbot: React.FC<ChatbotProps> = ({
apiUrl = '/api/chatbot/message',
title = 'Chat Support',
greeting = 'Hello! How can I help you?',
placeholder = 'Type your message...',
position = 'bottom-right',
theme = 'light',
maxMessages = 100,
onMessageSent,
onMessageReceived
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [messages, setMessages] = useState<Message[]>([
{
id: 1,
text: greeting,
sender: 'bot',
timestamp: Date.now()
}
]);
const [inputValue, setInputValue] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const conversationId = useRef<string>(
`conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const scrollToBottom = useCallback(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, []);
useEffect(() => {
scrollToBottom();
}, [messages, scrollToBottom]);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
const addMessage = useCallback((text: string, sender: 'user' | 'bot') => {
const newMessage: Message = {
id: Date.now() + Math.random(),
text,
sender,
timestamp: Date.now()
};
setMessages(prev => {
const updated = [...prev, newMessage];
// Limit message history
return updated.slice(-maxMessages);
});
if (sender === 'user' && onMessageSent) {
onMessageSent(text);
} else if (sender === 'bot' && onMessageReceived) {
onMessageReceived(text);
}
}, [maxMessages, onMessageSent, onMessageReceived]);
const sendMessage = useCallback(async () => {
if (!inputValue.trim() || isLoading) return;
const messageText = inputValue.trim();
setInputValue('');
setError(null);
addMessage(messageText, 'user');
setIsLoading(true);
try {
const csrfToken = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(csrfToken && { 'X-CSRF-TOKEN': csrfToken }),
},
body: JSON.stringify({
message: messageText,
conversation_id: conversationId.current,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data: ApiResponse = await response.json();
if (data.error) {
throw new Error(data.error);
}
addMessage(data.reply || 'I received your message but couldn\'t generate a response.', 'bot');
} catch (error) {
console.error('Chatbot error:', error);
const errorMessage = error instanceof Error
? `Error: ${error.message}`
: 'Sorry, I\'m having trouble connecting. Please try again.';
addMessage(errorMessage, 'bot');
setError(errorMessage);
} finally {
setIsLoading(false);
}
}, [inputValue, isLoading, apiUrl, addMessage]);
const handleKeyPress = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}, [sendMessage]);
const toggleChat = useCallback(() => {
setIsOpen(prev => !prev);
setError(null);
}, []);
const clearMessages = useCallback(() => {
setMessages([{
id: 1,
text: greeting,
sender: 'bot',
timestamp: Date.now()
}]);
setError(null);
}, [greeting]);
return (
<div className={`chatbot-container chatbot-${position} chatbot-theme-${theme}`}>
{isOpen && (
<div className="chatbot-popup">
<div className="chatbot-header">
<h4>{title}</h4>
<div className="chatbot-header-actions">
<button
className="chatbot-clear"
onClick={clearMessages}
aria-label="Clear conversation"
title="Clear conversation"
>
🗑️
</button>
<button
className="chatbot-close"
onClick={toggleChat}
aria-label="Close chat"
>
×
</button>
</div>
</div>
{error && (
<div className="chatbot-error">
<span>⚠️ {error}</span>
<button onClick={() => setError(null)}>×</button>
</div>
)}
<div className="chatbot-messages">
{messages.map((message) => (
<div
key={message.id}
className={`chatbot-message ${message.sender}-message`}
>
<div className="message-content">
{message.text}
</div>
<div className="message-time">
{new Date(message.timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
))}
{isLoading && (
<div className="chatbot-message bot-message">
<div className="message-content">
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className="chatbot-input">
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={placeholder}
disabled={isLoading}
maxLength={2000}
aria-label="Type your message"
/>
<button
onClick={sendMessage}
disabled={!inputValue.trim() || isLoading}
className="send-button"
aria-label="Send message"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
)}
<button
className="chatbot-toggle"
onClick={toggleChat}
aria-label={isOpen ? "Close chat" : "Open chat"}
>
{isOpen ? '×' : '💬'}
</button>
</div>
);
};
export default Chatbot;Create hooks/useChatbot.ts:
import { useState, useRef, useCallback, useEffect } from 'react';
interface Message {
id: number;
text: string;
sender: 'user' | 'bot';
timestamp: number;
}
interface UseChatbotOptions {
apiUrl: string;
greeting?: string;
maxMessages?: number;
onError?: (error: Error) => void;
}
export const useChatbot = ({
apiUrl,
greeting = 'Hello! How can I help you?',
maxMessages = 100,
onError
}: UseChatbotOptions) => {
const [messages, setMessages] = useState<Message[]>([
{ id: 1, text: greeting, sender: 'bot', timestamp: Date.now() }
]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const conversationId = useRef(
`conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
);
const addMessage = useCallback((text: string, sender: 'user' | 'bot') => {
const newMessage: Message = {
id: Date.now() + Math.random(),
text,
sender,
timestamp: Date.now()
};
setMessages(prev => {
const updated = [...prev, newMessage];
return updated.slice(-maxMessages);
});
}, [maxMessages]);
const sendMessage = useCallback(async (text: string) => {
if (!text.trim() || isLoading) return;
addMessage(text, 'user');
setIsLoading(true);
setError(null);
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: text,
conversation_id: conversationId.current,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
addMessage(data.reply || 'No response received.', 'bot');
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error');
setError(error.message);
addMessage('Sorry, I encountered an error.', 'bot');
if (onError) {
onError(error);
}
} finally {
setIsLoading(false);
}
}, [apiUrl, isLoading, addMessage, onError]);
const clearMessages = useCallback(() => {
setMessages([{ id: 1, text: greeting, sender: 'bot', timestamp: Date.now() }]);
setError(null);
}, [greeting]);
return {
messages,
isLoading,
error,
sendMessage,
clearMessages,
conversationId: conversationId.current
};
};import React from 'react';
import Chatbot from './components/Chatbot';
function App() {
return (
<div className="App">
<h1>My Website</h1>
<p>Welcome to my website!</p>
<Chatbot
apiUrl="/api/chatbot/message"
title="Customer Support"
greeting="Hi! How can I help you today?"
/>
</div>
);
}
export default App;import React from 'react';
import Chatbot from './components/Chatbot';
function App() {
const handleMessageSent = (message: string) => {
console.log('User sent:', message);
// Analytics tracking
gtag('event', 'chatbot_message_sent', {
message_length: message.length
});
};
const handleMessageReceived = (message: string) => {
console.log('Bot replied:', message);
// Analytics tracking
gtag('event', 'chatbot_message_received');
};
return (
<div className="App">
<Chatbot
apiUrl="/api/chatbot/message"
title="AI Assistant"
greeting="Hello! I'm your AI assistant. What can I help you with?"
placeholder="Ask me anything..."
position="bottom-right"
theme="dark"
maxMessages={50}
onMessageSent={handleMessageSent}
onMessageReceived={handleMessageReceived}
/>
</div>
);
}// pages/_app.tsx
import type { AppProps } from 'next/app';
import dynamic from 'next/dynamic';
const Chatbot = dynamic(() => import('../components/Chatbot'), {
ssr: false // Disable server-side rendering for chatbot
});
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Chatbot
apiUrl="/api/chatbot"
title="Help Center"
/>
</>
);
}
// pages/api/chatbot.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Your chatbot logic here
const { message } = req.body;
// Call your PHP backend or implement logic directly
const response = await fetch('http://localhost:8000/api/chatbot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
res.status(200).json(data);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
}Create components/Chatbot.vue:
<template>
<div :class="containerClasses">
<!-- Chat Popup -->
<Transition name="slide-up">
<div v-if="isOpen" class="chatbot-popup">
<!-- Header -->
<div class="chatbot-header">
<h4>{{ title }}</h4>
<div class="chatbot-header-actions">
<button
@click="clearMessages"
class="chatbot-clear"
aria-label="Clear conversation"
title="Clear conversation"
>
🗑️
</button>
<button
@click="toggleChat"
class="chatbot-close"
aria-label="Close chat"
>
×
</button>
</div>
</div>
<!-- Error Display -->
<div v-if="error" class="chatbot-error">
<span>⚠️ {{ error }}</span>
<button @click="clearError">×</button>
</div>
<!-- Messages -->
<div ref="messagesContainer" class="chatbot-messages">
<TransitionGroup name="message" tag="div">
<div
v-for="message in messages"
:key="message.id"
:class="getMessageClasses(message)"
>
<div class="message-content">
{{ message.text }}
</div>
<div class="message-time">
{{ formatTime(message.timestamp) }}
</div>
</div>
</TransitionGroup>
<!-- Typing Indicator -->
<div v-if="isLoading" class="chatbot-message bot-message">
<div class="message-content">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
<!-- Input -->
<div class="chatbot-input">
<input
ref="messageInput"
v-model="inputValue"
@keydown.enter.prevent="sendMessage"
:placeholder="placeholder"
:disabled="isLoading"
maxlength="2000"
type="text"
aria-label="Type your message"
/>
<button
@click="sendMessage"
:disabled="!canSendMessage"
class="send-button"
aria-label="Send message"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
</Transition>
<!-- Toggle Button -->
<button
@click="toggleChat"
class="chatbot-toggle"
:aria-label="isOpen ? 'Close chat' : 'Open chat'"
>
{{ isOpen ? '×' : '💬' }}
</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, watch, onMounted } from 'vue';
// Props
interface Props {
apiUrl?: string;
title?: string;
greeting?: string;
placeholder?: string;
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
theme?: 'light' | 'dark';
maxMessages?: number;
}
const props = withDefaults(defineProps<Props>(), {
apiUrl: '/api/chatbot/message',
title: 'Chat Support',
greeting: 'Hello! How can I help you?',
placeholder: 'Type your message...',
position: 'bottom-right',
theme: 'light',
maxMessages: 100
});
// Emits
interface Emits {
messageSent: [message: string];
messageReceived: [message: string];
error: [error: Error];
}
const emit = defineEmits<Emits>();
// Types
interface Message {
id: number;
text: string;
sender: 'user' | 'bot';
timestamp: number;
}
// Reactive state
const isOpen = ref(false);
const inputValue = ref('');
const isLoading = ref(false);
const error = ref<string | null>(null);
const messagesContainer = ref<HTMLElement>();
const messageInput = ref<HTMLInputElement>();
const conversationId = ref(
`conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
);
const messages = ref<Message[]>([
{
id: 1,
text: props.greeting,
sender: 'bot',
timestamp: Date.now()
}
]);
// Computed
const containerClasses = computed(() => [
'chatbot-container',
`chatbot-${props.position}`,
`chatbot-theme-${props.theme}`
]);
const canSendMessage = computed(() =>
inputValue.value.trim() && !isLoading.value
);
// Methods
const addMessage = (text: string, sender: 'user' | 'bot') => {
const newMessage: Message = {
id: Date.now() + Math.random(),
text,
sender,
timestamp: Date.now()
};
messages.value.push(newMessage);
// Limit message history
if (messages.value.length > props.maxMessages) {
messages.value = messages.value.slice(-props.maxMessages);
}
// Emit events
if (sender === 'user') {
emit('messageSent', text);
} else {
emit('messageReceived', text);
}
};
const sendMessage = async () => {
if (!canSendMessage.value) return;
const messageText = inputValue.value.trim();
inputValue.value = '';
error.value = null;
addMessage(messageText, 'user');
isLoading.value = true;
try {
const csrfToken = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
const response = await fetch(props.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(csrfToken && { 'X-CSRF-TOKEN': csrfToken }),
},
body: JSON.stringify({
message: messageText,
conversation_id: conversationId.value,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
addMessage(data.reply || 'No response received.', 'bot');
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
error.value = errorMessage;
addMessage('Sorry, I encountered an error.', 'bot');
emit('error', err instanceof Error ? err : new Error(errorMessage));
} finally {
isLoading.value = false;
}
};
const toggleChat = () => {
isOpen.value = !isOpen.value;
error.value = null;
};
const clearMessages = () => {
messages.value = [
{
id: 1,
text: props.greeting,
sender: 'bot',
timestamp: Date.now()
}
];
error.value = null;
};
const clearError = () => {
error.value = null;
};
const getMessageClasses = (message: Message) => [
'chatbot-message',
`${message.sender}-message`
];
const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
};
const scrollToBottom = () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
});
};
// Watchers
watch(messages, scrollToBottom, { deep: true });
watch(isOpen, (newValue) => {
if (newValue && messageInput.value) {
nextTick(() => {
messageInput.value?.focus();
});
}
});
// Lifecycle
onMounted(() => {
scrollToBottom();
});
</script>
<style scoped>
/* Transitions */
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter-from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
.slide-up-leave-to {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
.message-enter-active {
transition: all 0.3s ease;
}
.message-enter-from {
opacity: 0;
transform: translateY(10px);
}
/* Component styles - same as React version */
.chatbot-container {
position: fixed;
z-index: 1000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.chatbot-bottom-right {
bottom: 20px;
right: 20px;
}
.chatbot-bottom-left {
bottom: 20px;
left: 20px;
}
.chatbot-top-right {
top: 20px;
right: 20px;
}
.chatbot-top-left {
top: 20px;
left: 20px;
}
.chatbot-popup {
width: 350px;
height: 500px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
margin-bottom: 10px;
border: 1px solid #e1e5e9;
}
.chatbot-theme-dark .chatbot-popup {
background: #2c2c2c;
border-color: #404040;
color: #ffffff;
}
.chatbot-header {
padding: 16px 20px;
background: #4f46e5;
color: white;
border-radius: 12px 12px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.chatbot-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.chatbot-header-actions {
display: flex;
gap: 8px;
}
.chatbot-clear,
.chatbot-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: background-color 0.2s;
}
.chatbot-clear:hover,
.chatbot-close:hover {
background: rgba(255, 255, 255, 0.3);
}
.chatbot-error {
background: #fee2e2;
color: #991b1b;
padding: 12px;
margin: 12px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
.chatbot-error button {
background: none;
border: none;
color: #991b1b;
cursor: pointer;
font-size: 16px;
}
.chatbot-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.chatbot-message {
display: flex;
flex-direction: column;
max-width: 80%;
}
.user-message {
align-items: flex-end;
align-self: flex-end;
}
.bot-message {
align-items: flex-start;
align-self: flex-start;
}
.message-content {
padding: 10px 14px;
border-radius: 12px;
font-size: 14px;
line-height: 1.4;
word-wrap: break-word;
}
.user-message .message-content {
background: #4f46e5;
color: white;
border-bottom-right-radius: 4px;
}
.bot-message .message-content {
background: #f3f4f6;
color: #374151;
border-bottom-left-radius: 4px;
}
.chatbot-theme-dark .bot-message .message-content {
background: #404040;
color: #e5e5e5;
}
.message-time {
font-size: 11px;
color: #9ca3af;
margin-top: 4px;
}
.typing-indicator {
display: flex;
gap: 4px;
align-items: center;
padding: 4px 0;
}
.typing-indicator span {
width: 6px;
height: 6px;
background: #9ca3af;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-10px);
}
}
.chatbot-input {
padding: 16px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 8px;
align-items: center;
}
.chatbot-theme-dark .chatbot-input {
border-top-color: #404040;
}
.chatbot-input input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 20px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.chatbot-input input:focus {
border-color: #4f46e5;
}
.chatbot-theme-dark .chatbot-input input {
background: #404040;
border-color: #555;
color: #ffffff;
}
.chatbot-input input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.send-button {
width: 40px;
height: 40px;
background: #4f46e5;
border: none;
border-radius: 50%;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.send-button:hover:not(:disabled) {
background: #4338ca;
}
.send-button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.chatbot-toggle {
width: 60px;
height: 60px;
background: #4f46e5;
border: none;
border-radius: 50%;
color: white;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 16px rgba(79, 70, 229, 0.3);
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.chatbot-toggle:hover {
background: #4338ca;
transform: scale(1.05);
}
/* Mobile responsive */
@media (max-width: 480px) {
.chatbot-popup {
width: calc(100vw - 40px);
height: calc(100vh - 100px);
position: fixed;
bottom: 80px;
left: 20px;
right: 20px;
}
}
</style>Create composables/useChatbot.ts:
import { ref, computed, type Ref } from 'vue';
interface Message {
id: number;
text: string;
sender: 'user' | 'bot';
timestamp: number;
}
interface UseChatbotOptions {
apiUrl: string;
greeting?: string;
maxMessages?: number;
onError?: (error: Error) => void;
}
export function useChatbot(options: UseChatbotOptions) {
const {
apiUrl,
greeting = 'Hello! How can I help you?',
maxMessages = 100,
onError
} = options;
// State
const messages: Ref<Message[]> = ref([
{ id: 1, text: greeting, sender: 'bot', timestamp: Date.now() }
]);
const isLoading = ref(false);
const error = ref<string | null>(null);
const conversationId = ref(
`conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
);
// Computed
const lastMessage = computed(() =>
messages.value[messages.value.length - 1] || null
);
const messageCount = computed(() => messages.value.length);
// Methods
const addMessage = (text: string, sender: 'user' | 'bot') => {
const newMessage: Message = {
id: Date.now() + Math.random(),
text,
sender,
timestamp: Date.now()
};
messages.value.push(newMessage);
// Limit message history
if (messages.value.length > maxMessages) {
messages.value = messages.value.slice(-maxMessages);
}
};
const sendMessage = async (text: string): Promise<void> => {
if (!text.trim() || isLoading.value) return;
addMessage(text, 'user');
isLoading.value = true;
error.value = null;
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: text,
conversation_id: conversationId.value,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
addMessage(data.reply || 'No response received.', 'bot');
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
error.value = errorMessage;
addMessage('Sorry, I encountered an error.', 'bot');
if (onError && err instanceof Error) {
onError(err);
}
} finally {
isLoading.value = false;
}
};
const clearMessages = () => {
messages.value = [
{ id: 1, text: greeting, sender: 'bot', timestamp: Date.now() }
];
error.value = null;
};
const clearError = () => {
error.value = null;
};
return {
// State
messages: readonly(messages),
isLoading: readonly(isLoading),
error: readonly(error),
conversationId: readonly(conversationId),
// Computed
lastMessage,
messageCount,
// Methods
sendMessage,
addMessage,
clearMessages,
clearError
};
}<template>
<div id="app">
<h1>My Vue App</h1>
<p>Welcome to my application!</p>
<Chatbot
api-url="/api/chatbot/message"
title="Support Assistant"
greeting="Hello! How can I assist you today?"
position="bottom-right"
theme="light"
@message-sent="handleMessageSent"
@message-received="handleMessageReceived"
@error="handleError"
/>
</div>
</template>
<script setup lang="ts">
import Chatbot from './components/Chatbot.vue';
const handleMessageSent = (message: string) => {
console.log('User sent:', message);
};
const handleMessageReceived = (message: string) => {
console.log('Bot replied:', message);
};
const handleError = (error: Error) => {
console.error('Chatbot error:', error);
};
</script><template>
<div class="chat-page">
<div class="chat-header">
<h2>Customer Support</h2>
<p>Messages: {{ messageCount }}</p>
</div>
<div class="chat-messages">
<div
v-for="message in messages"
:key="message.id"
:class="['message', message.sender]"
>
{{ message.text }}
</div>
</div>
<div class="chat-input">
<input
v-model="inputText"
@keydown.enter="handleSend"
placeholder="Type a message..."
:disabled="isLoading"
/>
<button @click="handleSend" :disabled="!inputText.trim() || isLoading">
Send
</button>
</div>
<div v-if="error" class="error">
{{ error }}
<button @click="clearError">×</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useChatbot } from '@/composables/useChatbot';
const inputText = ref('');
const {
messages,
isLoading,
error,
messageCount,
sendMessage,
clearError
} = useChatbot({
apiUrl: '/api/chatbot/message',
greeting: 'Welcome! How can I help you?',
onError: (err) => {
console.error('Chat error:', err);
}
});
const handleSend = async () => {
if (!inputText.value.trim()) return;
await sendMessage(inputText.value);
inputText.value = '';
};
</script><!-- components/Chatbot.client.vue -->
<template>
<ClientOnly>
<Chatbot
:api-url="runtimeConfig.public.chatbotApiUrl"
title="Help Center"
:greeting="$t('chatbot.greeting')"
:placeholder="$t('chatbot.placeholder')"
@message-sent="trackMessageSent"
@message-received="trackMessageReceived"
/>
</ClientOnly>
</template>
<script setup lang="ts">
import Chatbot from './Chatbot.vue';
const runtimeConfig = useRuntimeConfig();
const { $gtag } = useNuxtApp();
const trackMessageSent = (message: string) => {
$gtag('event', 'chatbot_message_sent', {
message_length: message.length
});
};
const trackMessageReceived = (message: string) => {
$gtag('event', 'chatbot_message_received');
};
</script>
<!-- server/api/chatbot.post.ts -->
<script>
export default defineEventHandler(async (event) => {
const body = await readBody(event);
try {
// Your chatbot logic here
const response = await $fetch('http://localhost:8000/api/chatbot', {
method: 'POST',
body: {
message: body.message,
conversation_id: body.conversation_id
}
});
return response;
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'Chatbot service unavailable'
});
}
});
</script>
<!-- nuxt.config.ts -->
<script>
export default defineNuxtConfig({
runtimeConfig: {
public: {
chatbotApiUrl: process.env.CHATBOT_API_URL || '/api/chatbot'
}
}
});
</script>// stores/chatbot.ts
import { defineStore } from 'pinia';
import type { Message } from '@/types/chatbot';
export const useChatbotStore = defineStore('chatbot', () => {
const messages = ref<Message[]>([]);
const isLoading = ref(false);
const error = ref<string | null>(null);
const isOpen = ref(false);
const conversationHistory = computed(() =>
messages.value.filter(m => m.sender === 'user')
);
const addMessage = (message: Message) => {
messages.value.push(message);
};
const clearMessages = () => {
messages.value = [];
};
const toggleChat = () => {
isOpen.value = !isOpen.value;
};
return {
messages: readonly(messages),
isLoading: readonly(isLoading),
error: readonly(error),
isOpen: readonly(isOpen),
conversationHistory,
addMessage,
clearMessages,
toggleChat
};
});Create components/chatbot.component.ts:
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewChecked } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { BehaviorSubject, Subject, catchError, of, takeUntil } from 'rxjs';
interface Message {
id: number;
text: string;
sender: 'user' | 'bot';
timestamp: number;
}
interface ChatbotConfig {
apiUrl: string;
title: string;
greeting: string;
placeholder: string;
position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
theme: 'light' | 'dark';
maxMessages: number;
}
@Component({
selector: 'app-chatbot',
standalone: true,
imports: [CommonModule, FormsModule, HttpClientModule],
template: `
<div [class]="containerClasses">
<!-- Chat Popup -->
<div *ngIf="isOpen" class="chatbot-popup" [@slideUp]>
<!-- Header -->
<div class="chatbot-header">
<h4>{{ config.title }}</h4>
<div class="chatbot-header-actions">
<button
(click)="clearMessages()"
class="chatbot-clear"
aria-label="Clear conversation"
title="Clear conversation"
>
🗑️
</button>
<button
(click)="toggleChat()"
class="chatbot-close"
aria-label="Close chat"
>
×
</button>
</div>
</div>
<!-- Error Display -->
<div *ngIf="error$ | async as error" class="chatbot-error">
<span>⚠️ {{ error }}</span>
<button (click)="clearError()">×</button>
</div>
<!-- Messages -->
<div #messagesContainer class="chatbot-messages">
<div
*ngFor="let message of messages$ | async; trackBy: trackMessage"
[class]="getMessageClasses(message)"
[@messageSlide]
>
<div class="message-content">
{{ message.text }}
</div>
<div class="message-time">
{{ formatTime(message.timestamp) }}
</div>
</div>
<!-- Typing Indicator -->
<div *ngIf="isLoading$ | async" class="chatbot-message bot-message">
<div class="message-content">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
<!-- Input -->
<div class="chatbot-input">
<input
#messageInput
[(ngModel)]="inputValue"
(keydown.enter)="sendMessage()"
[placeholder]="config.placeholder"
[disabled]="isLoading$ | async"
maxlength="2000"
type="text"
aria-label="Type your message"
/>
<button
(click)="sendMessage()"
[disabled]="!canSendMessage()"
class="send-button"
aria-label="Send message"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
<!-- Toggle Button -->
<button
(click)="toggleChat()"
class="chatbot-toggle"
[attr.aria-label]="isOpen ? 'Close chat' : 'Open chat'"
>
{{ isOpen ? '×' : '💬' }}
</button>
</div>
`,
styleUrls: ['./chatbot.component.scss'],
animations: [
// Add your animations here
]
})
export class ChatbotComponent implements OnInit, OnDestroy, AfterViewChecked {
@Input() config: Partial<ChatbotConfig> = {};
@Output() messageSent = new EventEmitter<string>();
@Output() messageReceived = new EventEmitter<string>();
@Output() errorOccurred = new EventEmitter<Error>();
@ViewChild('messagesContainer') messagesContainer!: ElementRef;
@ViewChild('messageInput') messageInput!: ElementRef;
// Default configuration
private defaultConfig: ChatbotConfig = {
apiUrl: '/api/chatbot/message',
title: 'Chat Support',
greeting: 'Hello! How can I help you?',
placeholder: 'Type your message...',
position: 'bottom-right',
theme: 'light',
maxMessages: 100
};
// Component state
isOpen = false;
inputValue = '';
conversationId = `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Reactive state
private messages$ = new BehaviorSubject<Message[]>([]);
private isLoading$ = new BehaviorSubject<boolean>(false);
private error$ = new BehaviorSubject<string | null>(null);
private destroy$ = new Subject<void>();
private shouldScrollToBottom = false;
constructor(private http: HttpClient) {}
ngOnInit() {
// Initialize with greeting message
const initialMessage: Message = {
id: 1,
text: this.config.greeting || this.defaultConfig.greeting,
sender: 'bot',
timestamp: Date.now()
};
this.messages$.next([initialMessage]);
}
ngAfterViewChecked() {
if (this.shouldScrollToBottom) {
this.scrollToBottom();
this.shouldScrollToBottom = false;
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
// Getters for reactive state
get messages$() { return this.messages$.asObservable(); }
get isLoading$() { return this.isLoading$.asObservable(); }
get error$() { return this.error$.asObservable(); }
// Computed properties
get containerClasses(): string {
const position = this.config.position || this.defaultConfig.position;
const theme = this.config.theme || this.defaultConfig.theme;
return `chatbot-container chatbot-${position} chatbot-theme-${theme}`;
}
get mergedConfig(): ChatbotConfig {
return { ...this.defaultConfig, ...this.config };
}
// Methods
canSendMessage(): boolean {
return this.inputValue.trim().length > 0 && !this.isLoading$.value;
}
async sendMessage(): Promise<void> {
if (!this.canSendMessage()) return;
const messageText = this.inputValue.trim();
this.inputValue = '';
this.error$.next(null);
this.addMessage(messageText, 'user');
this.isLoading$.next(true);
const payload = {
message: messageText,
conversation_id: this.conversationId
};
this.http.post<any>(this.mergedConfig.apiUrl, payload)
.pipe(
catchError(error => {
console.error('Chatbot API error:', error);
return of({
error: error.status === 0
? 'Network error. Please check your connection.'
: `HTTP ${error.status}: ${error.statusText}`
});
}),
takeUntil(this.destroy$)
)
.subscribe({
next: (response) => {
if (response.error) {
this.handleError(new Error(response.error));
} else {
const reply = response.reply || 'No response received.';
this.addMessage(reply, 'bot');
this.messageReceived.emit(reply);
}
},
error: (error) => {
this.handleError(error);
},
complete: () => {
this.isLoading$.next(false);
}
});
}
private addMessage(text: string, sender: 'user' | 'bot'): void {
const newMessage: Message = {
id: Date.now() + Math.random(),
text,
sender,
timestamp: Date.now()
};
const currentMessages = this.messages$.value;
const updatedMessages = [...currentMessages, newMessage];
// Limit message history
const maxMessages = this.config.maxMessages || this.defaultConfig.maxMessages;
const limitedMessages = updatedMessages.slice(-maxMessages);
this.messages$.next(limitedMessages);
this.shouldScrollToBottom = true;
if (sender === 'user') {
this.messageSent.emit(text);
}
}
private handleError(error: Error): void {
this.error$.next(error.message);
this.addMessage('Sorry, I encountered an error. Please try again.', 'bot');
this.errorOccurred.emit(error);
}
toggleChat(): void {
this.isOpen = !this.isOpen;
this.error$.next(null);
if (this.isOpen) {
// Focus input after view update
setTimeout(() => {
if (this.messageInput?.nativeElement) {
this.messageInput.nativeElement.focus();
}
}, 100);
}
}
clearMessages(): void {
const greetingMessage: Message = {
id: 1,
text: this.config.greeting || this.defaultConfig.greeting,
sender: 'bot',
timestamp: Date.now()
};
this.messages$.next([greetingMessage]);
this.error$.next(null);
this.shouldScrollToBottom = true;
}
clearError(): void {
this.error$.next(null);
}
getMessageClasses(message: Message): string {
return `chatbot-message ${message.sender}-message`;
}
formatTime(timestamp: number): string {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
trackMessage(index: number, message: Message): number {
return message.id;
}
private scrollToBottom(): void {
if (this.messagesContainer?.nativeElement) {
const element = this.messagesContainer.nativeElement;
element.scrollTop = element.scrollHeight;
}
}
}Create services/chatbot.service.ts:
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
export interface Message {
id: number;
text: string;
sender: 'user' | 'bot';
timestamp: number;
}
export interface ChatbotResponse {
reply: string;
conversation_id?: string;
timestamp?: string;
error?: string;
}
export interface ChatbotConfig {
apiUrl: string;
greeting: string;
maxMessages: number;
retryAttempts: number;
}
@Injectable({
providedIn: 'root'
})
export class ChatbotService {
private messages$ = new BehaviorSubject<Message[]>([]);
private isLoading$ = new BehaviorSubject<boolean>(false);
private error$ = new BehaviorSubject<string | null>(null);
private config: ChatbotConfig;
private conversationId: string;
constructor(private http: HttpClient) {
this.config = {
apiUrl: '/api/chatbot/message',
greeting: 'Hello! How can I help you?',
maxMessages: 100,
retryAttempts: 3
};
this.conversationId = this.generateConversationId();
this.initializeWithGreeting();
}
// Observables
getMessages(): Observable<Message[]> {
return this.messages$.asObservable();
}
getLoadingState(): Observable<boolean> {
return this.isLoading$.asObservable();
}
getErrorState(): Observable<string | null> {
return this.error$.asObservable();
}
// Configuration
updateConfig(newConfig: Partial<ChatbotConfig>): void {
this.config = { ...this.config, ...newConfig };
}
getConfig(): ChatbotConfig {
return { ...this.config };
}
// Message management
sendMessage(text: string): Observable<Message> {
if (!text.trim() || this.isLoading$.value) {
return throwError(() => new Error('Invalid message or service busy'));
}
const userMessage = this.addMessage(text, 'user');
this.isLoading$.next(true);
this.error$.next(null);
const payload = {
message: text,
conversation_id: this.conversationId
};
return this.http.post<ChatbotResponse>(this.config.apiUrl, payload)
.pipe(
map(response => {
if (response.error) {
throw new Error(response.error);
}
return this.addMessage(response.reply || 'No response received.', 'bot');
}),
tap(() => this.isLoading$.next(false)),
catchError(error => {
this.isLoading$.next(false);
return this.handleError(error);
})
);
}
addMessage(text: string, sender: 'user' | 'bot'): Message {
const newMessage: Message = {
id: Date.now() + Math.random(),
text,
sender,
timestamp: Date.now()
};
const currentMessages = this.messages$.value;
const updatedMessages = [...currentMessages, newMessage];
// Limit message history
const limitedMessages = updatedMessages.slice(-this.config.maxMessages);
this.messages$.next(limitedMessages);
return newMessage;
}
clearMessages(): void {
this.initializeWithGreeting();
this.error$.next(null);
}
clearError(): void {
this.error$.next(null);
}
// Conversation management
getConversationId(): string {
return this.conversationId;
}
resetConversation(): void {
this.conversationId = this.generateConversationId();
this.clearMessages();
}
// Utility methods
getLastMessage(): Message | null {
const messages = this.messages$.value;
return messages.length > 0 ? messages[messages.length - 1] : null;
}
getMessageCount(): number {
return this.messages$.value.length;
}
getUserMessages(): Message[] {
return this.messages$.value.filter(m => m.sender === 'user');
}
getBotMessages(): Message[] {
return this.messages$.value.filter(m => m.sender === 'bot');
}
// Private methods
private generateConversationId(): string {
return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private initializeWithGreeting(): void {
const greetingMessage: Message = {
id: 1,
text: this.config.greeting,
sender: 'bot',
timestamp: Date.now()
};
this.messages$.next([greetingMessage]);
}
private handleError(error: HttpErrorResponse): Observable<Message> {
let errorMessage: string;
if (error.status === 0) {
errorMessage = 'Network error. Please check your connection.';
} else if (error.status >= 500) {
errorMessage = 'Server error. Please try again later.';
} else if (error.status === 429) {
errorMessage = 'Too many requests. Please wait a moment.';
} else {
errorMessage = error.error?.message || `Error ${error.status}: ${error.statusText}`;
}
this.error$.next(errorMessage);
const errorResponseMessage = this.addMessage(
'Sorry, I encountered an error. Please try again.',
'bot'
);
return throwError(() => new Error(errorMessage));
}
}// app.component.ts
import { Component } from '@angular/core';
import { ChatbotComponent } from './components/chatbot.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [ChatbotComponent],
template: `
<div class="app">
<header>
<h1>My Angular App</h1>
</header>
<main>
<p>Welcome to my application!</p>
</main>
<app-chatbot
[config]="chatbotConfig"
(messageSent)="onMessageSent($event)"
(messageReceived)="onMessageReceived($event)"
(errorOccurred)="onError($event)"
></app-chatbot>
</div>
`
})
export class AppComponent {
chatbotConfig = {
apiUrl: '/api/chatbot/message',
title: 'Support Assistant',
greeting: 'Hi! How can I help you today?',
position: 'bottom-right' as const,
theme: 'light' as const,
maxMessages: 50
};
onMessageSent(message: string) {
console.log('User sent:', message);
// Analytics tracking
}
onMessageReceived(message: string) {
console.log('Bot replied:', message);
// Analytics tracking
}
onError(error: Error) {
console.error('Chatbot error:', error);
// Error reporting
}
}// chat.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ChatbotService, Message } from '../services/chatbot.service';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="chat-container">
<div class="chat-header">
<h2>Customer Support</h2>
<div class="chat-stats">
<span>Messages: {{ messageCount$ | async }}</span>
<button (click)="clearConversation()">Clear</button>
</div>
</div>
<div class="chat-messages">
<div
*ngFor="let message of messages$ | async; trackBy: trackMessage"
[class]="'message ' + message.sender"
>
<div class="message-content">{{ message.text }}</div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
<div *ngIf="isLoading$ | async" class="loading">
AI is typing...
</div>
</div>
<div class="chat-input">
<input
[(ngModel)]="inputText"
(keydown.enter)="sendMessage()"
placeholder="Type your message..."
[disabled]="isLoading$ | async"
/>
<button
(click)="sendMessage()"
[disabled]="!inputText.trim() || (isLoading$ | async)"
>
Send
</button>
</div>
<div *ngIf="error$ | async as error" class="error">
{{ error }}
<button (click)="clearError()">×</button>
</div>
</div>
`,
styleUrls: ['./chat.component.scss']
})
export class ChatComponent implements OnInit, OnDestroy {
messages$: Observable<Message[]>;
isLoading$: Observable<boolean>;
error$: Observable<string | null>;
messageCount$: Observable<number>;
inputText = '';
private destroy$ = new Subject<void>();
constructor(private chatbotService: ChatbotService) {
this.messages$ = this.chatbotService.getMessages();
this.isLoading$ = this.chatbotService.getLoadingState();
this.error$ = this.chatbotService.getErrorState();
this.messageCount$ = this.messages$.pipe(
map(messages => messages.length)
);
}
ngOnInit() {
// Configure the chatbot service
this.chatbotService.updateConfig({
apiUrl: '/api/chatbot/message',
greeting: 'Welcome to our support chat!',
maxMessages: 100
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
sendMessage() {
if (!this.inputText.trim()) return;
this.chatbotService.sendMessage(this.inputText)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (message) => {
console.log('Message sent successfully:', message);
},
error: (error) => {
console.error('Failed to send message:', error);
}
});
this.inputText = '';
}
clearConversation() {
this.chatbotService.resetConversation();
}
clearError() {
this.chatbotService.clearError();
}
trackMessage(index: number, message: Message): number {
return message.id;
}
formatTime(timestamp: number): string {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
}Create chatbot.js:
class Chatbot {
constructor(options = {}) {
this.config = {
apiUrl: '/api/chatbot/message',
title: 'Chat Support',
greeting: 'Hello! How can I help you?',
placeholder: 'Type your message...',
position: 'bottom-right',
theme: 'light',
maxMessages: 100,
...options
};
this.isOpen = false;
this.isLoading = false;
this.messages = [];
this.conversationId = `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.init();
}
init() {
this.createElements();
this.attachEventListeners();
this.addMessage(this.config.greeting, 'bot');
}
createElements() {
// Create container
this.container = document.createElement('div');
this.container.className = `chatbot-container chatbot-${this.config.position} chatbot-theme-${this.config.theme}`;
// Create popup
this.popup = document.createElement('div');
this.popup.className = 'chatbot-popup';
this.popup.style.display = 'none';
// Create header
this.header = document.createElement('div');
this.header.className = 'chatbot-header';
this.header.innerHTML = `
<h4>${this.config.title}</h4>
<div class="chatbot-header-actions">
<button class="chatbot-clear" title="Clear conversation">🗑️</button>
<button class="chatbot-close">×</button>
</div>
`;
// Create messages container
this.messagesContainer = document.createElement('div');
this.messagesContainer.className = 'chatbot-messages';
// Create input container
this.inputContainer = document.createElement('div');
this.inputContainer.className = 'chatbot-input';
this.inputContainer.innerHTML = `
<input type="text" placeholder="${this.config.placeholder}" maxlength="2000">
<button class="send-button">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
`;
// Create toggle button
this.toggleButton = document.createElement('button');
this.toggleButton.className = 'chatbot-toggle';
this.toggleButton.textContent = '💬';
// Assemble components
this.popup.appendChild(this.header);
this.popup.appendChild(this.messagesContainer);
this.popup.appendChild(this.inputContainer);
this.container.appendChild(this.popup);
this.container.appendChild(this.toggleButton);
// Add to document
document.body.appendChild(this.container);
// Get references to interactive elements
this.input = this.inputContainer.querySelector('input');
this.sendButton = this.inputContainer.querySelector('.send-button');
this.clearButton = this.header.querySelector('.chatbot-clear');
this.closeButton = this.header.querySelector('.chatbot-close');
}
attachEventListeners() {
this.toggleButton.addEventListener('click', () => this.toggleChat());
this.closeButton.addEventListener('click', () => this.toggleChat());
this.clearButton.addEventListener('click', () => this.clearMessages());
this.sendButton.addEventListener('click', () => this.sendMessage());
this.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
}
toggleChat() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.popup.style.display = 'flex';
this.toggleButton.textContent = '×';
this.input.focus();
this.scrollToBottom();
} else {
this.popup.style.display = 'none';
this.toggleButton.textContent = '💬';
}
}
async sendMessage() {
const text = this.input.value.trim();
if (!text || this.isLoading) return;
this.input.value = '';
this.addMessage(text, 'user');
this.setLoading(true);
try {
const response = await fetch(this.config.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
},
body: JSON.stringify({
message: text,
conversation_id: this.conversationId
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
this.addMessage(data.reply || 'No response received.', 'bot');
// Emit custom event
this.container.dispatchEvent(new CustomEvent('messageReceived', {
detail: { message: data.reply }
}));
} catch (error) {
console.error('Chatbot error:', error);
this.addMessage('Sorry, I encountered an error. Please try again.', 'bot');
// Emit error event
this.container.dispatchEvent(new CustomEvent('error', {
detail: { error }
}));
} finally {
this.setLoading(false);
}
}
addMessage(text, sender) {
const message = {
id: Date.now() + Math.random(),
text,
sender,
timestamp: Date.now()
};
this.messages.push(message);
// Limit message history
if (this.messages.length > this.config.maxMessages) {
this.messages = this.messages.slice(-this.config.maxMessages);
this.renderMessages();
} else {
this.renderMessage(message);
}
this.scrollToBottom();
// Emit message event
this.container.dispatchEvent(new CustomEvent('messageSent', {
detail: { message: text, sender }
}));
}
renderMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = `chatbot-message ${message.sender}-message`;
messageElement.innerHTML = `
<div class="message-content">${this.escapeHtml(message.text)}</div>
<div class="message-time">${this.formatTime(message.timestamp)}</div>
`;
this.messagesContainer.appendChild(messageElement);
}
renderMessages() {
this.messagesContainer.innerHTML = '';
this.messages.forEach(message => this.renderMessage(message));
}
setLoading(loading) {
this.isLoading = loading;
this.input.disabled = loading;
this.sendButton.disabled = loading;
// Remove existing typing indicator
const existing = this.messagesContainer.querySelector('.typing-indicator-message');
if (existing) existing.remove();
if (loading) {
const typingElement = document.createElement('div');
typingElement.className = 'chatbot-message bot-message typing-indicator-message';
typingElement.innerHTML = `
<div class="message-content">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
`;
this.messagesContainer.appendChild(typingElement);
this.scrollToBottom();
}
}
clearMessages() {
this.messages = [];
this.renderMessages();
this.addMessage(this.config.greeting, 'bot');
}
scrollToBottom() {
setTimeout(() => {
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
}, 100);
}
formatTime(timestamp) {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Public API methods
open() {
if (!this.isOpen) this.toggleChat();
}
close() {
if (this.isOpen) this.toggleChat();
}
destroy() {
this.container.remove();
}
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
}
}
// Usage example
document.addEventListener('DOMContentLoaded', () => {
const chatbot = new Chatbot({
apiUrl: '/api/chatbot/message',
title: 'Customer Support',
greeting: 'Hi! How can I help you today?',
position: 'bottom-right',
theme: 'light'
});
// Listen to events
chatbot.container.addEventListener('messageSent', (e) => {
console.log('Message sent:', e.detail);
});
chatbot.container.addEventListener('messageReceived', (e) => {
console.log('Message received:', e.detail);
});
chatbot.container.addEventListener('error', (e) => {
console.error('Chatbot error:', e.detail);
});
});Create types/chatbot.d.ts:
// Core interfaces
export interface Message {
id: number;
text: string;
sender: 'user' | 'bot';
timestamp: number;
}
export interface ChatbotConfig {
apiUrl: string;
title: string;
greeting: string;
placeholder: string;
position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
theme: 'light' | 'dark';
maxMessages: number;
}
export interface ApiResponse {
reply: string;
conversation_id?: string;
timestamp?: string;
error?: string;
}
// Event interfaces
export interface MessageEvent {
message: string;
sender: 'user' | 'bot';
timestamp: number;
}
export interface ErrorEvent {
error: Error;
context?: string;
}
// Component prop interfaces
export interface ChatbotProps extends Partial<ChatbotConfig> {
onMessageSent?: (message: string) => void;
onMessageReceived?: (message: string) => void;
onError?: (error: Error) => void;
className?: string;
style?: React.CSSProperties;
}
// Hook interfaces
export interface UseChatbotOptions {
apiUrl: string;
greeting?: string;
maxMessages?: number;
onError?: (error: Error) => void;
}
export interface UseChatbotReturn {
messages: Message[];
isLoading: boolean;
error: string | null;
sendMessage: (text: string) => Promise<void>;
clearMessages: () => void;
conversationId: string;
}
// Service interfaces
export interface ChatbotService {
sendMessage(text: string): Promise<Message>;
getMessages(): Message[];
clearMessages(): void;
getConversationId(): string;
updateConfig(config: Partial<ChatbotConfig>): void;
}
// Global declarations for vanilla JS
declare global {
interface Window {
Chatbot: typeof Chatbot;
}
}
export class Chatbot {
constructor(options?: Partial<ChatbotConfig>);
open(): void;
close(): void;
destroy(): void;
updateConfig(config: Partial<ChatbotConfig>): void;
sendMessage(text: string): Promise<void>;
clearMessages(): void;
on(event: 'messageSent' | 'messageReceived' | 'error', handler: Function): void;
}Create styles/chatbot-themes.scss:
// Base variables
:root {
--chatbot-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
--chatbot-border-radius: 12px;
--chatbot-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
--chatbot-transition: all 0.2s ease;
--chatbot-z-index: 1000;
}
// Light theme (default)
.chatbot-theme-light {
--chatbot-primary-color: #4f46e5;
--chatbot-primary-hover: #4338ca;
--chatbot-background: #ffffff;
--chatbot-surface: #f9fafb;
--chatbot-border: #e5e7eb;
--chatbot-text-primary: #111827;
--chatbot-text-secondary: #6b7280;
--chatbot-text-muted: #9ca3af;
--chatbot-error: #ef4444;
--chatbot-error-bg: #fef2f2;
--chatbot-success: #10b981;
--chatbot-user-message-bg: var(--chatbot-primary-color);
--chatbot-user-message-text: #ffffff;
--chatbot-bot-message-bg: #f3f4f6;
--chatbot-bot-message-text: #374151;
--chatbot-input-bg: #ffffff;
--chatbot-input-border: #d1d5db;
--chatbot-input-focus: var(--chatbot-primary-color);
}
// Dark theme
.chatbot-theme-dark {
--chatbot-primary-color: #6366f1;
--chatbot-primary-hover: #5b21b6;
--chatbot-background: #1f2937;
--chatbot-surface: #374151;
--chatbot-border: #4b5563;
--chatbot-text-primary: #f9fafb;
--chatbot-text-secondary: #d1d5db;
--chatbot-text-muted: #9ca3af;
--chatbot-error: #f87171;
--chatbot-error-bg: #7f1d1d;
--chatbot-success: #34d399;
--chatbot-user-message-bg: var(--chatbot-primary-color);
--chatbot-user-message-text: #ffffff;
--chatbot-bot-message-bg: #4b5563;
--chatbot-bot-message-text: #e5e7eb;
--chatbot-input-bg: #374151;
--chatbot-input-border: #4b5563;
--chatbot-input-focus: var(--chatbot-primary-color);
}
// High contrast theme
.chatbot-theme-high-contrast {
--chatbot-primary-color: #000000;
--chatbot-primary-hover: #333333;
--chatbot-background: #ffffff;
--chatbot-surface: #f5f5f5;
--chatbot-border: #000000;
--chatbot-text-primary: #000000;
--chatbot-text-secondary: #333333;
--chatbot-text-muted: #666666;
--chatbot-error: #cc0000;
--chatbot-error-bg: #ffe6e6;
--chatbot-success: #006600;
--chatbot-user-message-bg: #000000;
--chatbot-user-message-text: #ffffff;
--chatbot-bot-message-bg: #f0f0f0;
--chatbot-bot-message-text: #000000;
--chatbot-input-bg: #ffffff;
--chatbot-input-border: #000000;
--chatbot-input-focus: #000000;
}Create styles/chatbot.css:
/* Import themes */
@import 'chatbot-themes.scss';
/* Base styles */
.chatbot-container {
position: fixed;
z-index: var(--chatbot-z-index);
font-family: var(--chatbot-font-family);
font-size: 14px;
line-height: 1.5;
}
/* Positioning */
.chatbot-bottom-right {
bottom: 20px;
right: 20px;
}
.chatbot-bottom-left {
bottom: 20px;
left: 20px;
}
.chatbot-top-right {
top: 20px;
right: 20px;
}
.chatbot-top-left {
top: 20px;
left: 20px;
}
/* Popup container */
.chatbot-popup {
width: 350px;
height: 500px;
background: var(--chatbot-background);
border-radius: var(--chatbot-border-radius);
box-shadow: var(--chatbot-shadow);
display: flex;
flex-direction: column;
margin-bottom: 10px;
border: 1px solid var(--chatbot-border);
overflow: hidden;
transform-origin: bottom right;
animation: chatbot-slide-up 0.3s ease;
}
@keyframes chatbot-slide-up {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Header */
.chatbot-header {
padding: 16px 20px;
background: var(--chatbot-primary-color);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.chatbot-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.chatbot-header-actions {
display: flex;
gap: 8px;
}
.chatbot-clear,
.chatbot-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: var(--chatbot-transition);
}
.chatbot-clear:hover,
.chatbot-close:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Error display */
.chatbot-error {
background: var(--chatbot-error-bg);
color: var(--chatbot-error);
padding: 12px;
margin: 12px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
border: 1px solid var(--chatbot-error);
}
.chatbot-error button {
background: none;
border: none;
color: var(--chatbot-error);
cursor: pointer;
font-size: 16px;
padding: 0;
margin-left: 8px;
}
/* Messages container */
.chatbot-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
scroll-behavior: smooth;
}
.chatbot-messages::-webkit-scrollbar {
width: 6px;
}
.chatbot-messages::-webkit-scrollbar-track {
background: transparent;
}
.chatbot-messages::-webkit-scrollbar-thumb {
background: var(--chatbot-text-muted);
border-radius: 3px;
}
.chatbot-messages::-webkit-scrollbar-thumb:hover {
background: var(--chatbot-text-secondary);
}
/* Individual messages */
.chatbot-message {
display: flex;
flex-direction: column;
max-width: 80%;
animation: chatbot-message-slide 0.3s ease;
}
@keyframes chatbot-message-slide {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.user-message {
align-items: flex-end;
align-self: flex-end;
}
.bot-message {
align-items: flex-start;
align-self: flex-start;
}
.message-content {
padding: 10px 14px;
border-radius: 12px;
font-size: 14px;
line-height: 1.4;
word-wrap: break-word;
white-space: pre-wrap;
}
.user-message .message-content {
background: var(--chatbot-user-message-bg);
color: var(--chatbot-user-message-text);
border-bottom-right-radius: 4px;
}
.bot-message .message-content {
background: var(--chatbot-bot-message-bg);
color: var(--chatbot-bot-message-text);
border-bottom-left-radius: 4px;
}
.message-time {
font-size: 11px;
color: var(--chatbot-text-muted);
margin-top: 4px;
opacity: 0.8;
}
/* Typing indicator */
.typing-indicator {
display: flex;
gap: 4px;
align-items: center;
padding: 4px 0;
}
.typing-indicator span {
width: 6px;
height: 6px;
background: var(--chatbot-text-muted);
border-radius: 50%;
animation: chatbot-typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes chatbot-typing {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-8px);
}
}
/* Input area */
.chatbot-input {
padding: 16px;
border-top: 1px solid var(--chatbot-border);
display: flex;
gap: 8px;
align-items: center;
flex-shrink: 0;
}
.chatbot-input input {
flex: 1;
padding: 10px 12px;
border: 1px solid var(--chatbot-input-border);
border-radius: 20px;
font-size: 14px;
outline: none;
transition: var(--chatbot-transition);
background: var(--chatbot-input-bg);
color: var(--chatbot-text-primary);
}
.chatbot-input input:focus {
border-color: var(--chatbot-input-focus);
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
}
.chatbot-input input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.chatbot-input input::placeholder {
color: var(--chatbot-text-muted);
}
/* Send button */
.send-button {
width: 40px;
height: 40px;
background: var(--chatbot-primary-color);
border: none;
border-radius: 50%;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--chatbot-transition);
flex-shrink: 0;
}
.send-button:hover:not(:disabled) {
background: var(--chatbot-primary-hover);
transform: scale(1.05);
}
.send-button:disabled {
background: var(--chatbot-text-muted);
cursor: not-allowed;
transform: none;
}
/* Toggle button */
.chatbot-toggle {
width: 60px;
height: 60px;
background: var(--chatbot-primary-color);
border: none;
border-radius: 50%;
color: white;
font-size: 24px;
cursor: pointer;
box-shadow: var(--chatbot-shadow);
transition: var(--chatbot-transition);
display: flex;
align-items: center;
justify-content: center;
}
.chatbot-toggle:hover {
background: var(--chatbot-primary-hover);
transform: scale(1.05);
}
.chatbot-toggle:active {
transform: scale(0.95);
}
/* Mobile responsive */
@media (max-width: 480px) {
.chatbot-popup {
width: calc(100vw - 40px);
height: calc(100vh - 100px);
max-height: 600px;
}
.chatbot-container {
left: 20px !important;
right: 20px !important;
bottom: 20px !important;
}
.chatbot-message {
max-width: 90%;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.chatbot-container {
--chatbot-border: #000000;
--chatbot-text-primary: #000000;
--chatbot-background: #ffffff;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.chatbot-popup,
.chatbot-message,
.chatbot-toggle {
animation: none;
transition: none;
}
.typing-indicator span {
animation: none;
}
}
/* Focus styles for accessibility */
.chatbot-toggle:focus,
.chatbot-clear:focus,
.chatbot-close:focus,
.send-button:focus {
outline: 2px solid var(--chatbot-primary-color);
outline-offset: 2px;
}
.chatbot-input input:focus {
outline: 2px solid var(--chatbot-primary-color);
outline-offset: 2px;
}// Custom theme example
const customTheme = {
primary: '#ff6b6b',
primaryHover: '#ff5252',
background: '#f8f9fa',
surface: '#ffffff',
border: '#dee2e6',
textPrimary: '#212529',
textSecondary: '#6c757d',
userMessageBg: '#ff6b6b',
userMessageText: '#ffffff',
botMessageBg: '#e9ecef',
botMessageText: '#495057'
};
// Apply custom theme
function applyCustomTheme(chatbotContainer, theme) {
const root = chatbotContainer;
Object.entries(theme).forEach(([key, value]) => {
const cssVar = `--chatbot-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
root.style.setProperty(cssVar, value);
});
}interface ChatbotCustomization {
avatar?: {
user?: string;
bot?: string;
};
sounds?: {
send?: string;
receive?: string;
error?: string;
};
animations?: {
messageEntry?: string;
typing?: string;
};
features?: {
fileUpload?: boolean;
voiceInput?: boolean;
suggestions?: string[];
typing?: boolean;
};
}
class AdvancedChatbot extends Chatbot {
constructor(options: ChatbotOptions & { customization?: ChatbotCustomization }) {
super(options);
this.customization = options.customization || {};
this.setupCustomFeatures();
}
private setupCustomFeatures() {
if (this.customization.features?.fileUpload) {
this.addFileUploadFeature();
}
if (this.customization.features?.voiceInput) {
this.addVoiceInputFeature();
}
if (this.customization.features?.suggestions) {
this.addSuggestionsFeature();
}
}
private addFileUploadFeature() {
// Implementation for file upload
}
private addVoiceInputFeature() {
// Implementation for voice input
}
private addSuggestionsFeature() {
// Implementation for message suggestions
}
}This comprehensive frontend integration guide provides:
✅ Complete React components with TypeScript support and hooks
✅ Full Vue 3 components with Composition API and composables
✅ Angular standalone components with services and RxJS
✅ Vanilla JavaScript implementation for any framework
✅ TypeScript definitions for type safety
✅ Complete styling system with themes and customization
✅ Accessibility features and responsive design
✅ Advanced customization options for enterprise use
Each implementation includes real-world examples, best practices, and framework-specific patterns to help developers integrate the chatbot seamlessly into their applications.
Next recommended reading: Examples for real-world implementation scenarios.