Spaces:
Paused
Paused
| <html lang="uz"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | |
| <title>Help.me - Tez Tibbiy Yordam</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> | |
| <style> | |
| /* ==================== ROOT VARIABLES ==================== */ | |
| :root { | |
| --primary-blue: #4A90E2; | |
| --primary-green: #50C878; | |
| --accent-red: #EF4444; | |
| --bg-white: #FFFFFF; | |
| --bg-light: #F8FAFC; | |
| --bg-gray: #F1F5F9; | |
| --text-dark: #1E293B; | |
| --text-medium: #475569; | |
| --text-light: #94A3B8; | |
| --border-color: #E2E8F0; | |
| --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1); | |
| --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); | |
| } | |
| /* ==================== GLOBAL RESET ==================== */ | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: linear-gradient(135deg, var(--primary-blue) 0%, var(--primary-green) 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 1rem; | |
| } | |
| /* ==================== CONTAINER ==================== */ | |
| .app-container { | |
| width: 100%; | |
| max-width: 480px; | |
| height: 95vh; | |
| max-height: 900px; | |
| background: var(--bg-white); | |
| border-radius: 24px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| animation: slideUp 0.5s ease-out; | |
| } | |
| @keyframes slideUp { | |
| from { | |
| transform: translateY(50px); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| } | |
| /* ==================== HEADER ==================== */ | |
| .app-header { | |
| background: linear-gradient(135deg, var(--primary-blue), var(--primary-green)); | |
| padding: 1.25rem 1.5rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| box-shadow: var(--shadow-md); | |
| position: relative; | |
| z-index: 10; | |
| } | |
| .header-left { | |
| flex: 1; | |
| } | |
| .header-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| color: white; | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| margin-bottom: 0.25rem; | |
| } | |
| .header-subtitle { | |
| color: rgba(255, 255, 255, 0.9); | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| } | |
| .dispatcher-link { | |
| width: 40px; | |
| height: 40px; | |
| background: rgba(255, 255, 255, 0.2); | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 1.25rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-decoration: none; | |
| } | |
| .dispatcher-link:hover { | |
| background: rgba(255, 255, 255, 0.3); | |
| transform: scale(1.05); | |
| } | |
| /* ==================== LANGUAGE SELECTOR ==================== */ | |
| .language-selector { | |
| background: var(--bg-light); | |
| padding: 0.875rem 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .language-label { | |
| color: var(--text-medium); | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| } | |
| .language-buttons { | |
| display: flex; | |
| gap: 0.5rem; | |
| flex: 1; | |
| } | |
| .lang-btn { | |
| flex: 1; | |
| padding: 0.5rem; | |
| border: 2px solid var(--border-color); | |
| background: white; | |
| border-radius: 8px; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: var(--text-medium); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.25rem; | |
| } | |
| .lang-btn:hover { | |
| border-color: var(--primary-blue); | |
| background: var(--bg-light); | |
| } | |
| .lang-btn.active { | |
| border-color: var(--primary-blue); | |
| background: var(--primary-blue); | |
| color: white; | |
| } | |
| /* ==================== CONNECTION STATUS ==================== */ | |
| .connection-status { | |
| padding: 0.625rem 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| font-size: 0.8rem; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| } | |
| .connection-status.connected { | |
| background: #ECFDF5; | |
| color: #059669; | |
| } | |
| .connection-status.disconnected { | |
| background: #FEF2F2; | |
| color: #DC2626; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| animation: pulse 2s infinite; | |
| } | |
| .status-dot.connected { | |
| background: #10B981; | |
| } | |
| .status-dot.disconnected { | |
| background: #EF4444; | |
| } | |
| @keyframes pulse { | |
| 0%, | |
| 100% { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| 50% { | |
| opacity: 0.5; | |
| transform: scale(0.95); | |
| } | |
| } | |
| /* ==================== CHAT CONTAINER ==================== */ | |
| .chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 1.5rem; | |
| background: var(--bg-gray); | |
| } | |
| .chat-container::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .chat-container::-webkit-scrollbar-thumb { | |
| background: var(--border-color); | |
| border-radius: 3px; | |
| } | |
| /* ==================== MESSAGES ==================== */ | |
| .message { | |
| display: flex; | |
| gap: 0.75rem; | |
| margin-bottom: 1.25rem; | |
| animation: messageSlide 0.3s ease-out; | |
| } | |
| @keyframes messageSlide { | |
| from { | |
| opacity: 0; | |
| transform: translateY(15px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .message.user { | |
| flex-direction: row-reverse; | |
| } | |
| .message-avatar { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.25rem; | |
| flex-shrink: 0; | |
| } | |
| .message.ai .message-avatar { | |
| background: linear-gradient(135deg, var(--primary-blue), var(--primary-green)); | |
| color: white; | |
| } | |
| .message.user .message-avatar { | |
| background: var(--bg-light); | |
| color: var(--text-medium); | |
| } | |
| .message-content { | |
| max-width: 75%; | |
| padding: 0.875rem 1.125rem; | |
| border-radius: 16px; | |
| font-size: 0.9375rem; | |
| line-height: 1.5; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .message.ai .message-content { | |
| background: white; | |
| color: var(--text-dark); | |
| border-bottom-left-radius: 4px; | |
| } | |
| .message.user .message-content { | |
| background: var(--primary-blue); | |
| color: white; | |
| border-bottom-right-radius: 4px; | |
| } | |
| /* ==================== TYPING INDICATOR ==================== */ | |
| .typing-indicator { | |
| display: none; | |
| align-items: center; | |
| gap: 0.75rem; | |
| margin-bottom: 1.25rem; | |
| } | |
| .typing-indicator.show { | |
| display: flex; | |
| } | |
| .typing-dots { | |
| display: flex; | |
| gap: 4px; | |
| padding: 0.875rem 1.125rem; | |
| background: white; | |
| border-radius: 16px; | |
| border-bottom-left-radius: 4px; | |
| } | |
| .typing-dots span { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--text-light); | |
| border-radius: 50%; | |
| animation: typingBounce 1.4s infinite; | |
| } | |
| .typing-dots span:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .typing-dots span:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| @keyframes typingBounce { | |
| 0%, | |
| 60%, | |
| 100% { | |
| transform: translateY(0); | |
| } | |
| 30% { | |
| transform: translateY(-8px); | |
| } | |
| } | |
| /* ==================== HINT BOX ==================== */ | |
| .hint-box { | |
| background: #EEF2FF; | |
| border-left: 4px solid var(--primary-blue); | |
| padding: 0.875rem 1rem; | |
| margin: 1rem 1.5rem; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .hint-icon { | |
| font-size: 1.25rem; | |
| color: var(--primary-blue); | |
| } | |
| .hint-text { | |
| color: var(--text-medium); | |
| font-size: 0.875rem; | |
| line-height: 1.4; | |
| } | |
| .hint-text strong { | |
| color: var(--text-dark); | |
| font-weight: 600; | |
| } | |
| /* ==================== VOICE CONTROL ==================== */ | |
| .voice-control { | |
| padding: 1.5rem; | |
| background: white; | |
| border-top: 1px solid var(--border-color); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .voice-button { | |
| width: 80px; | |
| height: 80px; | |
| border-radius: 50%; | |
| border: none; | |
| background: linear-gradient(135deg, var(--accent-red), #DC2626); | |
| color: white; | |
| font-size: 2rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 8px 20px rgba(239, 68, 68, 0.3); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| } | |
| .voice-button:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 10px 25px rgba(239, 68, 68, 0.4); | |
| } | |
| .voice-button:active { | |
| transform: scale(0.95); | |
| } | |
| .voice-button.recording { | |
| background: linear-gradient(135deg, #DC2626, #B91C1C); | |
| animation: recordingPulse 1.5s infinite; | |
| } | |
| @keyframes recordingPulse { | |
| 0%, | |
| 100% { | |
| box-shadow: 0 8px 20px rgba(239, 68, 68, 0.3), | |
| 0 0 0 0 rgba(239, 68, 68, 0.7); | |
| } | |
| 50% { | |
| box-shadow: 0 10px 25px rgba(239, 68, 68, 0.4), | |
| 0 0 0 20px rgba(239, 68, 68, 0); | |
| } | |
| } | |
| .voice-status { | |
| text-align: center; | |
| } | |
| .voice-status-text { | |
| color: var(--text-dark); | |
| font-size: 0.9375rem; | |
| font-weight: 600; | |
| margin-bottom: 0.25rem; | |
| } | |
| .voice-status-hint { | |
| color: var(--text-light); | |
| font-size: 0.8125rem; | |
| } | |
| /* ==================== RESPONSIVE ==================== */ | |
| @media (max-width: 480px) { | |
| .app-container { | |
| height: 100vh; | |
| max-height: 100vh; | |
| border-radius: 0; | |
| } | |
| .app-header { | |
| border-radius: 0; | |
| } | |
| .voice-button { | |
| width: 70px; | |
| height: 70px; | |
| font-size: 1.75rem; | |
| } | |
| } | |
| @media (max-height: 700px) { | |
| .hint-box { | |
| display: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Header --> | |
| <div class="app-header"> | |
| <div class="header-left"> | |
| <div class="header-title"> | |
| <i class="bi bi-heart-pulse-fill"></i> | |
| <span id="header-main-title">HELP.ME</span> | |
| </div> | |
| <div class="header-subtitle" id="header-subtitle">Tez Tibbiy Yordam</div> | |
| </div> | |
| <a href="/dispatcher" class="dispatcher-link" title="Admin Panel"> | |
| <i class="bi bi-gear-fill"></i> | |
| </a> | |
| </div> | |
| <!-- Language Selector --> | |
| <div class="language-selector"> | |
| <span class="language-label" id="lang-label">Interface:</span> | |
| <div class="language-buttons"> | |
| <button class="lang-btn active" data-lang="uz" onclick="changeLanguage('uz')"> | |
| πΊπΏ O'zbek | |
| </button> | |
| <button class="lang-btn" data-lang="en" onclick="changeLanguage('en')"> | |
| π¬π§ English | |
| </button> | |
| <button class="lang-btn" data-lang="ru" onclick="changeLanguage('ru')"> | |
| π·πΊ Π ΡΡΡΠΊΠΈΠΉ | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Connection Status --> | |
| <div id="connection-status" class="connection-status disconnected"> | |
| <span class="status-dot disconnected"></span> | |
| <span id="status-text">Serverga ulanmoqda...</span> | |
| </div> | |
| <!-- Chat Container --> | |
| <div id="chat-container" class="chat-container"> | |
| <!-- Typing Indicator --> | |
| <div id="typing-indicator" class="typing-indicator"> | |
| <div class="message-avatar" | |
| style="background: linear-gradient(135deg, var(--primary-blue), var(--primary-green)); color: white;"> | |
| <i class="bi bi-stethoscope"></i> | |
| </div> | |
| <div class="typing-dots"> | |
| <span></span> | |
| <span></span> | |
| <span></span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Hint Box --> | |
| <div class="hint-box"> | |
| <i class="bi bi-info-circle-fill hint-icon"></i> | |
| <div class="hint-text" id="hint-text"> | |
| <strong>3 tilda</strong> gaplashishingiz mumkin: O'zbekcha, Inglizcha yoki Ruscha | |
| </div> | |
| </div> | |
| <!-- Voice Control --> | |
| <div class="voice-control"> | |
| <button id="record-button" class="voice-button" onclick="toggleRecording()"> | |
| <i class="bi bi-mic-fill"></i> | |
| </button> | |
| <div class="voice-status"> | |
| <div class="voice-status-text" id="voice-status-text">Gapirish uchun bosing</div> | |
| <div class="voice-status-hint" id="voice-status-hint">Tugmani bosib, muammoingizni ayting</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ==================== GLOBAL STATE ==================== | |
| let ws = null; | |
| let caseId = null; | |
| let mediaRecorder = null; | |
| let audioChunks = []; | |
| let isRecording = false; | |
| let currentLanguage = 'uz'; | |
| const translations = { | |
| uz: { | |
| headerTitle: 'HELP.ME', | |
| headerSubtitle: 'Tez Tibbiy Yordam', | |
| langLabel: 'Interface:', | |
| statusConnected: 'Ulangan', | |
| statusDisconnected: 'Serverga ulanmoqda...', | |
| hintText: '<strong>3 tilda</strong> gaplashishingiz mumkin: O\'zbekcha, Inglizcha yoki Ruscha', | |
| voiceStart: 'Gapirish uchun bosing', | |
| voiceRecording: 'Yozilmoqda... To\'xtatish uchun bosing', | |
| voiceHint: 'Tugmani bosib, muammoingizni ayting' | |
| }, | |
| en: { | |
| headerTitle: 'HELP.ME', | |
| headerSubtitle: 'Emergency Medical Service', | |
| langLabel: 'Interface:', | |
| statusConnected: 'Connected', | |
| statusDisconnected: 'Connecting to server...', | |
| hintText: '<strong>3 languages</strong> supported: Uzbek, English or Russian', | |
| voiceStart: 'Press to speak', | |
| voiceRecording: 'Recording... Press to stop', | |
| voiceHint: 'Press button and describe your problem' | |
| }, | |
| ru: { | |
| headerTitle: 'HELP.ME', | |
| headerSubtitle: 'Π‘ΠΊΠΎΡΠ°Ρ ΠΠ΅Π΄ΠΈΡΠΈΠ½ΡΠΊΠ°Ρ ΠΠΎΠΌΠΎΡΡ', | |
| langLabel: 'ΠΠ½ΡΠ΅ΡΡΠ΅ΠΉΡ:', | |
| statusConnected: 'ΠΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΎ', | |
| statusDisconnected: 'ΠΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅ ΠΊ ΡΠ΅ΡΠ²Π΅ΡΡ...', | |
| hintText: '<strong>3 ΡΠ·ΡΠΊΠ°</strong> ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°ΡΡΡΡ: Π£Π·Π±Π΅ΠΊΡΠΊΠΈΠΉ, ΠΠ½Π³Π»ΠΈΠΉΡΠΊΠΈΠΉ ΠΈΠ»ΠΈ Π ΡΡΡΠΊΠΈΠΉ', | |
| voiceStart: 'ΠΠ°ΠΆΠΌΠΈΡΠ΅ ΡΡΠΎΠ±Ρ Π³ΠΎΠ²ΠΎΡΠΈΡΡ', | |
| voiceRecording: 'ΠΠ°ΠΏΠΈΡΡ... ΠΠ°ΠΆΠΌΠΈΡΠ΅ Π΄Π»Ρ ΠΎΡΡΠ°Π½ΠΎΠ²ΠΊΠΈ', | |
| voiceHint: 'ΠΠ°ΠΆΠΌΠΈΡΠ΅ ΠΊΠ½ΠΎΠΏΠΊΡ ΠΈ ΠΎΠΏΠΈΡΠΈΡΠ΅ ΠΏΡΠΎΠ±Π»Π΅ΠΌΡ' | |
| } | |
| }; | |
| // ==================== DOM ELEMENTS ==================== | |
| const chatContainer = document.getElementById('chat-container'); | |
| const typingIndicator = document.getElementById('typing-indicator'); | |
| const recordButton = document.getElementById('record-button'); | |
| const connectionStatus = document.getElementById('connection-status'); | |
| // ==================== INITIALIZATION ==================== | |
| window.addEventListener('DOMContentLoaded', function () { | |
| console.log('π Help.me interface loaded'); | |
| connectWebSocket(); | |
| updateClock(); | |
| setInterval(updateClock, 60000); | |
| }); | |
| // ==================== LANGUAGE SWITCHING ==================== | |
| function changeLanguage(lang) { | |
| currentLanguage = lang; | |
| // Update buttons | |
| document.querySelectorAll('.lang-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| document.querySelector(`[data-lang="${lang}"]`).classList.add('active'); | |
| // Update UI text | |
| const t = translations[lang]; | |
| document.getElementById('header-main-title').textContent = t.headerTitle; | |
| document.getElementById('header-subtitle').textContent = t.headerSubtitle; | |
| document.getElementById('lang-label').textContent = t.langLabel; | |
| document.getElementById('hint-text').innerHTML = t.hintText; | |
| document.getElementById('voice-status-text').textContent = t.voiceStart; | |
| document.getElementById('voice-status-hint').textContent = t.voiceHint; | |
| // Update status if needed | |
| if (ws && ws.readyState === WebSocket.OPEN) { | |
| document.getElementById('status-text').textContent = t.statusConnected; | |
| } else { | |
| document.getElementById('status-text').textContent = t.statusDisconnected; | |
| } | |
| console.log('π Language changed to:', lang); | |
| } | |
| // ==================== WEBSOCKET ==================== | |
| function connectWebSocket() { | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const wsUrl = `${protocol}//${window.location.host}/ws/chat`; | |
| console.log('π Connecting to WebSocket:', wsUrl); | |
| ws = new WebSocket(wsUrl); | |
| ws.onopen = function () { | |
| console.log('β WebSocket connected'); | |
| updateConnectionStatus(true); | |
| }; | |
| ws.onmessage = function (event) { | |
| if (typeof event.data === 'string') { | |
| try { | |
| const message = JSON.parse(event.data); | |
| handleWebSocketMessage(message); | |
| } catch (e) { | |
| console.error('β JSON parse error:', e); | |
| } | |
| } | |
| }; | |
| ws.onerror = function (error) { | |
| console.error('β WebSocket error:', error); | |
| }; | |
| ws.onclose = function () { | |
| console.log('π WebSocket disconnected'); | |
| updateConnectionStatus(false); | |
| setTimeout(connectWebSocket, 3000); | |
| }; | |
| } | |
| function updateConnectionStatus(connected) { | |
| const t = translations[currentLanguage]; | |
| const statusEl = document.getElementById('connection-status'); | |
| const statusText = document.getElementById('status-text'); | |
| const statusDot = statusEl.querySelector('.status-dot'); | |
| if (connected) { | |
| statusEl.classList.remove('disconnected'); | |
| statusEl.classList.add('connected'); | |
| statusDot.classList.remove('disconnected'); | |
| statusDot.classList.add('connected'); | |
| statusText.textContent = t.statusConnected; | |
| } else { | |
| statusEl.classList.remove('connected'); | |
| statusEl.classList.add('disconnected'); | |
| statusDot.classList.remove('connected'); | |
| statusDot.classList.add('disconnected'); | |
| statusText.textContent = t.statusDisconnected; | |
| } | |
| } | |
| function handleWebSocketMessage(message) { | |
| console.log('π¨ Message received:', message.type); | |
| switch (message.type) { | |
| case 'welcome': | |
| caseId = message.case_id; | |
| console.log('β Case ID:', caseId); | |
| break; | |
| case 'transcription': | |
| hideTypingIndicator(); | |
| addMessage('user', message.text); | |
| showTypingIndicator(); | |
| break; | |
| case 'ai_response': | |
| hideTypingIndicator(); | |
| addMessage('ai', message.text); | |
| if (message.audio_url) { | |
| playAudioResponse(message.audio_url); | |
| } | |
| break; | |
| case 'error': | |
| hideTypingIndicator(); | |
| console.error('β Server error:', message.message); | |
| break; | |
| default: | |
| console.log('π¦ Unknown message type:', message.type); | |
| } | |
| } | |
| // ==================== VOICE RECORDING ==================== | |
| async function toggleRecording() { | |
| if (isRecording) { | |
| stopRecording(); | |
| } else { | |
| await startRecording(); | |
| } | |
| } | |
| async function startRecording() { | |
| try { | |
| if (!ws || ws.readyState !== WebSocket.OPEN) { | |
| alert('WebSocket ulanmagan. Iltimos, kuting...'); | |
| return; | |
| } | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder = new MediaRecorder(stream); | |
| audioChunks = []; | |
| mediaRecorder.ondataavailable = function (event) { | |
| if (event.data.size > 0) { | |
| audioChunks.push(event.data); | |
| } | |
| }; | |
| mediaRecorder.onstop = function () { | |
| const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); | |
| sendAudioToServer(audioBlob); | |
| stream.getTracks().forEach(track => track.stop()); | |
| }; | |
| mediaRecorder.start(); | |
| isRecording = true; | |
| recordButton.classList.add('recording'); | |
| recordButton.innerHTML = '<i class="bi bi-stop-fill"></i>'; | |
| const t = translations[currentLanguage]; | |
| document.getElementById('voice-status-text').textContent = t.voiceRecording; | |
| console.log('π€ Recording started'); | |
| } catch (error) { | |
| console.error('β Microphone error:', error); | |
| alert('Mikrofonni ishlatib bo\'lmadi. Ruxsat bering.'); | |
| } | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && isRecording) { | |
| mediaRecorder.stop(); | |
| isRecording = false; | |
| recordButton.classList.remove('recording'); | |
| recordButton.innerHTML = '<i class="bi bi-mic-fill"></i>'; | |
| const t = translations[currentLanguage]; | |
| document.getElementById('voice-status-text').textContent = t.voiceStart; | |
| console.log('π€ Recording stopped'); | |
| showTypingIndicator(); | |
| } | |
| } | |
| async function sendAudioToServer(audioBlob) { | |
| try { | |
| const arrayBuffer = await audioBlob.arrayBuffer(); | |
| const uint8Array = new Uint8Array(arrayBuffer); | |
| ws.send(uint8Array); | |
| ws.send('__END__'); | |
| console.log('π€ Audio sent:', uint8Array.length, 'bytes'); | |
| } catch (error) { | |
| console.error('β Error sending audio:', error); | |
| hideTypingIndicator(); | |
| } | |
| } | |
| // ==================== MESSAGES ==================== | |
| function addMessage(sender, text) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${sender}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.innerHTML = sender === 'ai' | |
| ? '<i class="bi bi-stethoscope"></i>' | |
| : '<i class="bi bi-person-fill"></i>'; | |
| const content = document.createElement('div'); | |
| content.className = 'message-content'; | |
| content.textContent = text; | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(content); | |
| chatContainer.insertBefore(messageDiv, typingIndicator); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| function showTypingIndicator() { | |
| typingIndicator.classList.add('show'); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| function hideTypingIndicator() { | |
| typingIndicator.classList.remove('show'); | |
| } | |
| function playAudioResponse(audioUrl) { | |
| try { | |
| console.log('π Playing audio:', audioUrl); | |
| const audio = new Audio(audioUrl); | |
| audio.play().catch(error => { | |
| console.error('β Audio play error:', error); | |
| }); | |
| } catch (error) { | |
| console.error('β playAudioResponse error:', error); | |
| } | |
| } | |
| // ==================== UTILITIES ==================== | |
| function updateClock() { | |
| // Optional: Add clock if needed | |
| } | |
| </script> | |
| </body> | |
| </html> |