Spaces:
Runtime error
Runtime error
| <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> |