smart-line-bot / app /api /v1 /endpoints /widget.html
Smiel2's picture
Initial commit
2eae977 verified
Raw
History Blame Contribute Delete
11.2 kB
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客服機器人 Widget</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#chat-widget-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 99999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#chat-widget-toggle {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #06c755 0%, #00b44a 100%);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
border: none;
}
#chat-widget-toggle:hover {
transform: scale(1.1);
}
#chat-widget-toggle svg {
width: 30px;
height: 30px;
fill: white;
}
#chat-widget-window {
position: absolute;
bottom: 80px;
right: 0;
width: 360px;
height: 500px;
background: white;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
display: none;
flex-direction: column;
overflow: hidden;
}
#chat-widget-window.open {
display: flex;
}
.widget-header {
background: linear-gradient(135deg, #06c755 0%, #00b44a 100%);
color: white;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.widget-header-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.widget-header-info h3 {
font-size: 16px;
font-weight: 600;
}
.widget-header-info p {
font-size: 12px;
opacity: 0.9;
}
.widget-close {
margin-left: auto;
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 20px;
}
.widget-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 80%;
padding: 12px 16px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
}
.message.bot {
background: #f0f0f0;
align-self: flex-start;
border-bottom-left-radius: 4px;
}
.message.user {
background: #06c755;
color: white;
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.message-time {
font-size: 10px;
opacity: 0.7;
margin-top: 4px;
}
.widget-input-area {
padding: 12px;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 8px;
}
.widget-input {
flex: 1;
padding: 12px 16px;
border: 1px solid #e0e0e0;
border-radius: 24px;
font-size: 14px;
outline: none;
}
.widget-input:focus {
border-color: #06c755;
}
.widget-send {
width: 44px;
height: 44px;
border-radius: 50%;
background: #06c755;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.widget-send:hover {
background: #00b44a;
}
.widget-send svg {
width: 20px;
height: 20px;
fill: white;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 12px 16px;
background: #f0f0f0;
border-radius: 16px;
border-bottom-left-radius: 4px;
width: fit-content;
}
.typing-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #666;
animation: typing 1.4s infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
}
</style>
</head>
<body>
<div id="chat-widget-container">
<div id="chat-widget-window">
<div class="widget-header">
<div class="widget-header-avatar">🤖</div>
<div class="widget-header-info">
<h3>客服機器人</h3>
<p>線上服務</p>
</div>
<button class="widget-close" onclick="toggleWidget()">×</button>
</div>
<div class="widget-messages" id="widget-messages">
<div class="message bot">
👋 你好!我是智能客服機器人
<div class="message-time">現在</div>
</div>
</div>
<div class="widget-input-area">
<input type="text" class="widget-input" id="widget-input"
placeholder="輸入訊息..." onkeypress="handleKeyPress(event)">
<button class="widget-send" onclick="sendMessage()">
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
</div>
<button id="chat-widget-toggle" onclick="toggleWidget()">
<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
</button>
</div>
<script>
const API_URL = '/api/v1/widget';
let sessionId = localStorage.getItem('widget_session') ||
'session_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('widget_session', sessionId);
let ws = null;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
function toggleWidget() {
const window = document.getElementById('chat-widget-window');
window.classList.toggle('open');
const toggle = document.getElementById('chat-widget-toggle');
if (window.classList.contains('open')) {
toggle.style.display = 'none';
connectWebSocket();
} else {
toggle.style.display = 'flex';
disconnectWebSocket();
}
}
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}${API_URL}/ws/${sessionId}`;
try {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
reconnectAttempts = 0;
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message' || data.type === 'broadcast') {
addMessage(data.message, 'bot', data.timestamp);
}
};
ws.onclose = () => {
console.log('WebSocket closed');
if (document.getElementById('chat-widget-window').classList.contains('open')
&& reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
setTimeout(connectWebSocket, 2000);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
} catch (e) {
console.error('WebSocket connection error:', e);
}
}
function disconnectWebSocket() {
if (ws) {
ws.close();
ws = null;
}
}
function sendMessage() {
const input = document.getElementById('widget-input');
const message = input.value.trim();
if (!message) return;
addMessage(message, 'user');
input.value = '';
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
message: message
}));
} else {
// Fallback to REST API
fetch(`${API_URL}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
message: message
})
})
.then(res => res.json())
.then(data => {
if (data.message) {
addMessage(data.message, 'bot');
}
});
}
}
function addMessage(text, sender, timestamp = null) {
const container = document.getElementById('widget-messages');
const time = timestamp ? new Date(timestamp).toLocaleTimeString('zh-TW', {
hour: '2-digit', minute: '2-digit'
}) : '現在';
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}`;
messageDiv.innerHTML = `${text}<div class="message-time">${time}</div>`;
container.appendChild(messageDiv);
container.scrollTop = container.scrollHeight;
}
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
</script>
</body>
</html>