chat-ui.html•21.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BitCommerz AI Assistant - Testing Interface</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.chat-container {
width: 90%;
max-width: 800px;
height: 80vh;
background: white;
border-radius: 10px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h1 {
font-size: 1.5rem;
font-weight: 600;
}
.settings {
display: flex;
gap: 10px;
align-items: center;
}
.settings label {
font-size: 0.9rem;
}
.shop-selector {
position: relative;
display: inline-block;
}
.shop-search {
padding: 5px 10px;
border-radius: 5px;
border: none;
background: rgba(255,255,255,0.2);
color: white;
width: 200px;
cursor: pointer;
}
.shop-search::placeholder {
color: rgba(255,255,255,0.7);
}
.shop-dropdown {
position: absolute;
top: 100%;
right: 0;
background: white;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
max-height: 300px;
overflow-y: auto;
display: none;
z-index: 1000;
width: 250px;
margin-top: 5px;
}
.shop-dropdown.active {
display: block;
}
.shop-item {
padding: 10px;
color: #333;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.shop-item:hover {
background: #f5f5f5;
}
.shop-item.selected {
background: #e8f4ff;
font-weight: bold;
}
.shop-item-info {
font-size: 0.8rem;
color: #666;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f7f8fa;
}
.message {
margin-bottom: 15px;
display: flex;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
justify-content: flex-end;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
}
.message.user .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 5px;
}
.message.assistant .message-content {
background: white;
color: #333;
border-bottom-left-radius: 5px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.message-label {
font-size: 0.75rem;
color: #666;
margin-bottom: 4px;
padding-left: 16px;
}
.message.user .message-label {
text-align: right;
padding-right: 16px;
padding-left: 0;
}
.chat-input {
padding: 20px;
background: white;
border-top: 1px solid #e0e0e0;
}
.input-wrapper {
display: flex;
gap: 10px;
}
.chat-input input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 25px;
font-size: 1rem;
outline: none;
transition: border-color 0.3s;
}
.chat-input input:focus {
border-color: #667eea;
}
.chat-input button {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
transition: transform 0.2s;
}
.chat-input button:hover {
transform: scale(1.05);
}
.chat-input button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.typing-indicator {
display: none;
padding: 20px;
color: #666;
}
.typing-indicator.active {
display: block;
}
.typing-indicator span {
animation: blink 1.4s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0%, 60%, 100% { opacity: 0.3; }
30% { opacity: 1; }
}
.error-message {
background: #ff5252;
color: white;
padding: 10px;
border-radius: 5px;
margin: 10px 20px;
display: none;
}
.error-message.show {
display: block;
}
.metadata {
font-size: 0.75rem;
color: #999;
margin-top: 4px;
font-style: italic;
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<h1>BitCommerz AI Assistant</h1>
<div class="settings">
<label>Shop:</label>
<div class="shop-selector">
<input
type="text"
class="shop-search"
id="shopSearch"
placeholder="Search or select shop..."
readonly
/>
<div class="shop-dropdown" id="shopDropdown"></div>
</div>
</div>
</div>
<div class="chat-messages" id="chatMessages">
<div class="message assistant">
<div>
<div class="message-label">Assistant</div>
<div class="message-content">
Hello! I'm your BitCommerz AI assistant. Ask me anything about your store data - products, sales, inventory, customers, or any other metrics!
</div>
</div>
</div>
</div>
<div class="typing-indicator" id="typingIndicator">
Assistant is typing<span>.</span><span>.</span><span>.</span>
</div>
<div class="error-message" id="errorMessage"></div>
<div class="chat-input">
<div class="input-wrapper">
<input
type="text"
id="messageInput"
placeholder="Type your message here..."
autocomplete="off"
/>
<button id="sendButton">Send</button>
</div>
</div>
</div>
<script>
// Configuration
const API_URL = 'http://localhost:8000/api/v1/chat/enhanced';
const SHOPS_API_URL = 'http://localhost:8000/api/v1/chat/shops';
// State
let conversationId = null;
let isProcessing = false;
let allShops = [];
let selectedShopId = 10; // Default shop
// Elements
const chatMessages = document.getElementById('chatMessages');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const typingIndicator = document.getElementById('typingIndicator');
const errorMessage = document.getElementById('errorMessage');
const shopSearch = document.getElementById('shopSearch');
const shopDropdown = document.getElementById('shopDropdown');
// Add message to chat
function addMessage(content, isUser = false, metadata = null) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isUser ? 'user' : 'assistant'}`;
let metadataHtml = '';
if (metadata && !isUser) {
metadataHtml = `
<div class="metadata">
${metadata.model_used ? `Model: ${metadata.model_used} | ` : ''}
${metadata.execution_time_ms ? `Time: ${metadata.execution_time_ms}ms | ` : ''}
${metadata.tools_called ? `Tools: ${metadata.tools_called.join(', ')}` : ''}
</div>
`;
}
messageDiv.innerHTML = `
<div>
<div class="message-label">${isUser ? 'You' : 'Assistant'}</div>
<div class="message-content">
${content}
${metadataHtml}
</div>
</div>
`;
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Show/hide typing indicator
function setTyping(show) {
typingIndicator.classList.toggle('active', show);
if (show) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
// Show error
function showError(message) {
errorMessage.textContent = message;
errorMessage.classList.add('show');
setTimeout(() => {
errorMessage.classList.remove('show');
}, 5000);
}
// Load shops from API
async function loadShops() {
try {
const response = await fetch(SHOPS_API_URL);
const data = await response.json();
if (data.success) {
allShops = data.shops;
renderShopDropdown(allShops);
// Set default shop
const defaultShop = allShops.find(s => s.id === 10) || allShops[0];
if (defaultShop) {
selectShop(defaultShop.id, defaultShop.name);
}
}
} catch (error) {
console.error('Failed to load shops:', error);
// Use fallback shops
allShops = [
{id: 10, name: 'Shop 10'},
{id: 1, name: 'Shop 1'},
{id: 2, name: 'Shop 2'}
];
renderShopDropdown(allShops);
}
}
// Render shop dropdown
function renderShopDropdown(shops) {
shopDropdown.innerHTML = '';
shops.forEach(shop => {
const item = document.createElement('div');
item.className = 'shop-item';
if (shop.id === selectedShopId) {
item.classList.add('selected');
}
item.innerHTML = `
<div>${shop.name} (ID: ${shop.id})</div>
${shop.status ? `<div class="shop-item-info">Status: ${shop.status}</div>` : ''}
${shop.order_count ? `<div class="shop-item-info">${shop.order_count} orders</div>` : ''}
${shop.product_count ? `<div class="shop-item-info">${shop.product_count} products</div>` : ''}
`;
item.onclick = () => {
selectShop(shop.id, shop.name);
closeShopDropdown();
};
shopDropdown.appendChild(item);
});
}
// Select a shop
function selectShop(shopId, shopName) {
selectedShopId = shopId;
shopSearch.value = shopName;
conversationId = null; // Reset conversation
// Update selected state in dropdown
document.querySelectorAll('.shop-item').forEach(item => {
item.classList.remove('selected');
});
addMessage(
`Switched to ${shopName}. Starting a new conversation.`,
false
);
}
// Filter shops based on search
function filterShops(searchTerm) {
const filtered = allShops.filter(shop =>
shop.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
shop.id.toString().includes(searchTerm)
);
renderShopDropdown(filtered);
}
// Open shop dropdown
function openShopDropdown() {
shopDropdown.classList.add('active');
shopSearch.removeAttribute('readonly');
shopSearch.value = '';
shopSearch.focus();
renderShopDropdown(allShops);
}
// Close shop dropdown
function closeShopDropdown() {
shopDropdown.classList.remove('active');
shopSearch.setAttribute('readonly', true);
const currentShop = allShops.find(s => s.id === selectedShopId);
if (currentShop) {
shopSearch.value = currentShop.name;
}
}
// Send message
async function sendMessage(message = null, isDisambiguation = false, originalQuery = null, selectedIntent = null) {
message = message || messageInput.value.trim();
if (!message && !isDisambiguation) return;
if (isProcessing) return;
// Add user message if not disambiguation
if (!isDisambiguation) {
addMessage(message, true);
messageInput.value = '';
}
// Disable input
isProcessing = true;
sendButton.disabled = true;
setTyping(true);
try {
let requestBody = {
shop_id: selectedShopId,
conversation_id: conversationId
};
if (isDisambiguation) {
requestBody.query = '';
requestBody.selected_intent = selectedIntent;
requestBody.original_query = originalQuery;
} else {
requestBody.query = message;
}
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
const data = await response.json();
if (data.success) {
conversationId = data.conversation_id;
// Handle disambiguation
if (data.needs_clarification) {
handleDisambiguation(data);
} else {
addMessage(data.response, false, data.metadata);
}
} else {
addMessage(
data.response || 'Sorry, I encountered an error processing your request.',
false
);
if (data.error) {
showError(`Error: ${data.error}`);
}
}
} catch (error) {
console.error('Error:', error);
addMessage(
'Sorry, I couldn\'t connect to the server. Please make sure the API is running.',
false
);
showError(`Connection error: ${error.message}`);
} finally {
setTyping(false);
isProcessing = false;
sendButton.disabled = false;
messageInput.focus();
}
}
// Handle disambiguation response
function handleDisambiguation(data) {
const disambiguationDiv = document.createElement('div');
disambiguationDiv.className = 'message assistant';
let optionsHtml = '';
data.options.forEach((option, index) => {
optionsHtml += `
<button class="disambiguation-button"
data-intent="${option.intent}"
data-original="${data.metadata.original_query}"
style="display: block; margin: 5px 0; padding: 10px 15px;
background: white; border: 2px solid #667eea;
border-radius: 10px; cursor: pointer; width: 100%;
text-align: left; transition: all 0.2s;">
${index + 1}. ${option.description}
</button>
`;
});
disambiguationDiv.innerHTML = `
<div>
<div class="message-label">Assistant</div>
<div class="message-content">
<div>${data.question}</div>
<div style="margin-top: 10px;">
${optionsHtml}
</div>
</div>
</div>
`;
chatMessages.appendChild(disambiguationDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
// Add event listeners to buttons
const buttons = disambiguationDiv.querySelectorAll('.disambiguation-button');
buttons.forEach(button => {
button.addEventListener('click', function() {
const intent = this.getAttribute('data-intent');
const original = this.getAttribute('data-original');
// Add user's selection as a message
addMessage(`Selected: ${this.textContent}`, true);
// Disable all buttons
buttons.forEach(b => b.disabled = true);
// Send disambiguation response
sendMessage(null, true, original, intent);
});
button.addEventListener('mouseenter', function() {
this.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
this.style.color = 'white';
});
button.addEventListener('mouseleave', function() {
this.style.background = 'white';
this.style.color = 'black';
});
});
}
// Event listeners
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Shop dropdown event listeners
shopSearch.addEventListener('click', (e) => {
e.stopPropagation();
if (shopDropdown.classList.contains('active')) {
closeShopDropdown();
} else {
openShopDropdown();
}
});
shopSearch.addEventListener('input', (e) => {
filterShops(e.target.value);
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.shop-selector')) {
closeShopDropdown();
}
});
// Load shops on page load
loadShops();
// Focus on input when page loads
messageInput.focus();
// Example queries
const exampleQueries = [
"How many products do we have?",
"What's our total revenue?",
"Show me low stock products",
"How much revenue in July?",
"What are our top selling products?",
"How many active customers?",
"What's our average order value?"
];
// Add a help message with examples
setTimeout(() => {
addMessage(
`Try asking me questions like:<br><br>` +
exampleQueries.map(q => `• "${q}"`).join('<br>'),
false
);
}, 1000);
</script>
</body>
</html>