Spaces:
Sleeping
Sleeping
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RAG Chat Demo</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: #f5f5f5; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| } | |
| .panel { | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| h2 { | |
| color: #333; | |
| margin-bottom: 15px; | |
| font-size: 18px; | |
| border-bottom: 2px solid #007bff; | |
| padding-bottom: 10px; | |
| } | |
| .form-group { | |
| margin-bottom: 15px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 5px; | |
| color: #555; | |
| font-weight: 500; | |
| font-size: 14px; | |
| } | |
| input[type="text"], textarea { | |
| width: 100%; | |
| padding: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| font-family: inherit; | |
| font-size: 14px; | |
| resize: none; | |
| } | |
| input[type="text"]:focus, textarea:focus { | |
| outline: none; | |
| border-color: #007bff; | |
| box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); | |
| } | |
| button { | |
| background: #007bff; | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| font-size: 14px; | |
| transition: background 0.2s; | |
| } | |
| button:hover { background: #0056b3; } | |
| button:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .output { | |
| flex: 1; | |
| background: #f9f9f9; | |
| border: 1px solid #eee; | |
| border-radius: 4px; | |
| padding: 15px; | |
| overflow-y: auto; | |
| max-height: 400px; | |
| font-size: 14px; | |
| line-height: 1.6; | |
| color: #333; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| } | |
| .status { | |
| font-size: 12px; | |
| color: #666; | |
| margin-top: 10px; | |
| padding-top: 10px; | |
| border-top: 1px solid #eee; | |
| } | |
| .status.success { color: #28a745; } | |
| .status.error { color: #dc3545; } | |
| @media (max-width: 900px) { | |
| .container { grid-template-columns: 1fr; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- Panel 1: Non-streaming (JSON) --> | |
| <div class="panel"> | |
| <h2>📤 Non-Streaming (JSON)</h2> | |
| <div class="form-group"> | |
| <label>Session ID:</label> | |
| <input type="text" id="sessionNonStream" value="user_session_1" /> | |
| </div> | |
| <div class="form-group"> | |
| <label>Message:</label> | |
| <input type="text" id="messageNonStream" placeholder="Nhập câu hỏi..." /> | |
| </div> | |
| <button onclick="sendNonStream()">Gửi (JSON Response)</button> | |
| <div class="output" id="outputNonStream">Đợi phản hồi...</div> | |
| <div class="status" id="statusNonStream"></div> | |
| </div> | |
| <!-- Panel 2: Streaming (SSE) --> | |
| <div class="panel"> | |
| <h2>📨 Streaming (SSE)</h2> | |
| <div class="form-group"> | |
| <label>Session ID:</label> | |
| <input type="text" id="sessionStream" value="user_session_1" /> | |
| </div> | |
| <div class="form-group"> | |
| <label>Message:</label> | |
| <input type="text" id="messageStream" placeholder="Nhập câu hỏi..." /> | |
| </div> | |
| <button onclick="sendStream()" id="btnStream">Gửi (Streaming)</button> | |
| <div class="output" id="outputStream">Đợi phản hồi...</div> | |
| <div class="status" id="statusStream"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_BASE = "http://localhost:8000"; | |
| // Non-streaming: POST /chat (JSON response) | |
| async function sendNonStream() { | |
| const sessionId = document.getElementById('sessionNonStream').value.trim(); | |
| const message = document.getElementById('messageNonStream').value.trim(); | |
| if (!message) { | |
| setStatus('statusNonStream', 'Nhập tin nhắn', 'error'); | |
| return; | |
| } | |
| const outputDiv = document.getElementById('outputNonStream'); | |
| const statusDiv = document.getElementById('statusNonStream'); | |
| outputDiv.textContent = 'Đang xử lý...'; | |
| statusDiv.textContent = ''; | |
| try { | |
| const response = await fetch(`${API_BASE}/chat`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ session_id: sessionId, message }) | |
| }); | |
| if (!response.ok) { | |
| const err = await response.json(); | |
| throw new Error(err.detail || 'Lỗi server'); | |
| } | |
| const data = await response.json(); | |
| outputDiv.textContent = data.response || 'Không có phản hồi'; | |
| setStatus('statusNonStream', '✅ Hoàn thành', 'success'); | |
| } catch (err) { | |
| outputDiv.textContent = `❌ Lỗi: ${err.message}`; | |
| setStatus('statusNonStream', `Lỗi: ${err.message}`, 'error'); | |
| } | |
| } | |
| // Streaming: POST /chat/stream (SSE) | |
| async function sendStream() { | |
| const sessionId = document.getElementById('sessionStream').value.trim(); | |
| const message = document.getElementById('messageStream').value.trim(); | |
| if (!message) { | |
| setStatus('statusStream', 'Nhập tin nhắn', 'error'); | |
| return; | |
| } | |
| const outputDiv = document.getElementById('outputStream'); | |
| const statusDiv = document.getElementById('statusStream'); | |
| const btnStream = document.getElementById('btnStream'); | |
| outputDiv.textContent = ''; | |
| statusDiv.textContent = 'Đang kết nối...'; | |
| btnStream.disabled = true; | |
| try { | |
| const response = await fetch(`${API_BASE}/chat/stream`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ session_id: sessionId, message }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); // Giữ lại dòng chưa hoàn chỉnh | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| try { | |
| const jsonStr = line.substring(6); | |
| const data = JSON.parse(jsonStr); | |
| if (data.done) { | |
| setStatus('statusStream', '✅ Hoàn thành streaming', 'success'); | |
| btnStream.disabled = false; | |
| break; | |
| } | |
| if (data.delta) { | |
| outputDiv.textContent += data.delta; | |
| outputDiv.scrollTop = outputDiv.scrollHeight; | |
| } | |
| if (data.error) { | |
| throw new Error(data.error); | |
| } | |
| } catch (parseErr) { | |
| console.warn('Parse error:', parseErr); | |
| } | |
| } | |
| } | |
| } | |
| if (!outputDiv.textContent) { | |
| outputDiv.textContent = 'Không nhận được phản hồi'; | |
| } | |
| btnStream.disabled = false; | |
| } catch (err) { | |
| outputDiv.textContent = `❌ Lỗi: ${err.message}`; | |
| setStatus('statusStream', `Lỗi: ${err.message}`, 'error'); | |
| btnStream.disabled = false; | |
| } | |
| } | |
| function setStatus(elementId, message, type) { | |
| const el = document.getElementById(elementId); | |
| el.textContent = message; | |
| el.className = `status ${type}`; | |
| } | |
| // Gợi ý: Nhấn Enter để gửi | |
| document.getElementById('messageNonStream').addEventListener('keypress', e => { | |
| if (e.key === 'Enter') sendNonStream(); | |
| }); | |
| document.getElementById('messageStream').addEventListener('keypress', e => { | |
| if (e.key === 'Enter') sendStream(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |