Help_Me_3 / static /bemor /index.html
giyos1212's picture
Upload 72 files
98b6d67 verified
<!DOCTYPE html>
<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="icon" href="/static/favicon.ico" type="image/x-icon">
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<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 {
--bg-dark: #0f172a;
--bg-medium: #1e293b;
--bg-light: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--accent-red: #ef4444;
--accent-green: #10b981;
--accent-blue: #3b82f6;
--accent-purple: #8b5cf6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: var(--text-primary);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 1rem;
overflow: hidden;
}
.app-container {
width: 100%;
max-width: 420px;
height: 90vh;
max-height: 850px;
background: var(--bg-dark);
border-radius: 24px;
box-shadow: 0 50px 100px -20px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.1);
animation: slideInFromBottom 0.5s ease-out;
}
@keyframes slideInFromBottom {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.app-header {
padding: 16px 20px;
text-align: center;
flex-shrink: 0;
position: relative;
background: var(--bg-medium);
border-bottom: 1px solid var(--bg-light);
}
.app-header h1 {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
color: var(--accent-blue);
}
/* YANGI: Admin paneli tugmasi uchun yaxshilangan stillar */
.admin-panel-btn {
position: absolute;
top: 12px;
right: 12px;
background: var(--accent-blue);
color: white;
border: none;
border-radius: 8px;
padding: 6px 10px;
font-size: 0.75rem;
font-weight: 600;
text-decoration: none;
transition: all 0.2s ease;
animation: pulse-admin 2s infinite;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.admin-panel-btn:hover {
background: var(--accent-blue);
color: white;
transform: scale(1.05);
}
/* YANGI: Admin tugmasi uchun pulsatsiya animatsiyasi */
@keyframes pulse-admin {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
.connection-status {
padding: 8px 16px;
text-align: center;
font-size: 0.8rem;
flex-shrink: 0;
transition: all 0.3s ease;
background: var(--bg-medium);
}
.connection-status.connected {
color: var(--accent-green);
}
.connection-status.disconnected {
color: var(--accent-red);
}
.chat-container {
flex: 1;
overflow-y: auto;
padding: 20px;
background: var(--bg-dark);
}
.message {
margin-bottom: 16px;
display: flex;
gap: 10px;
animation: messageSlide 0.3s ease-out;
}
@keyframes messageSlide {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
flex-direction: row-reverse;
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
flex-shrink: 0;
}
.message.ai .message-avatar {
background: linear-gradient(135deg, var(--accent-blue), #2563eb);
}
.message.user .message-avatar {
background: linear-gradient(135deg, var(--accent-green), #059669);
}
.message-content {
max-width: 75%;
padding: 12px 16px;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
}
.message.ai .message-content {
background: var(--bg-medium);
color: var(--text-primary);
border-bottom-left-radius: 4px;
}
.message.user .message-content {
background: var(--accent-blue);
color: white;
border-bottom-right-radius: 4px;
}
.typing-indicator {
display: none;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--bg-medium);
border-radius: 16px;
width: fit-content;
margin-bottom: 16px;
}
.typing-indicator.show {
display: flex;
}
.typing-dots {
display: flex;
gap: 4px;
}
.typing-dots span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-secondary);
animation: typingBounce 1.4s infinite ease-in-out both;
}
.typing-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-dots span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typingBounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
}
.input-container {
background: var(--bg-dark);
padding: 20px;
border-top: 1px solid var(--bg-light);
flex-shrink: 0;
}
.recording-mode {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.record-button-wrapper {
position: relative;
width: 90px;
height: 90px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.record-button {
width: 80px;
height: 80px;
border-radius: 50%;
border: none;
background: var(--accent-red);
color: white;
font-size: 2rem;
cursor: pointer;
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.5);
transition: all 0.3s ease;
z-index: 5;
animation: pulse-red 2s infinite;
}
@keyframes pulse-red {
0% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
}
}
.record-button.recording {
background: var(--accent-green);
animation: none;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.5);
}
.visualizer-ring {
position: absolute;
width: 90px;
height: 90px;
border: 2px solid var(--accent-green);
border-radius: 50%;
opacity: 0;
transform: scale(1);
}
.recording .visualizer-ring {
animation: wave 2s infinite ease-out;
}
.visualizer-ring:nth-child(2) {
animation-delay: 0.5s;
}
.visualizer-ring:nth-child(3) {
animation-delay: 1s;
}
.visualizer-ring:nth-child(4) {
animation-delay: 1.5s;
}
@keyframes wave {
0% {
transform: scale(1);
opacity: 0.7;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
.recording-status {
text-align: center;
color: var(--text-secondary);
font-size: 0.9rem;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
}
.lang-switcher {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 8px;
margin-bottom: 8px;
}
.lang-btn {
background: var(--bg-light);
border: none;
color: var(--text-secondary);
padding: 4px 12px;
border-radius: 16px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.lang-btn:hover {
background: var(--bg-light);
color: var(--text-primary);
}
.lang-btn.active {
background: var(--accent-blue);
color: white;
}
</style>
</head>
<body>
<div class="app-container">
<!-- App Header -->
<div class="app-header">
<a href="/dispatcher" target="_blank" class="admin-panel-btn" title="Dispetcher Panel">
<i class="bi bi-shield-lock-fill"></i> Admin Panel
</a>
<h1 data-translate-key="appTitle">
Tez Tibbiy Yordam
</h1>
</div>
<!-- Connection Status -->
<div id="connection-status" class="connection-status disconnected">
<i class="bi bi-circle-fill" style="font-size: 0.6rem; margin-right: 6px;"></i>
<span data-translate-key="connecting">Serverga ulanmoqda...</span>
</div>
<!-- Chat Messages -->
<div id="chat-container" class="chat-container">
<!-- Salomlashuv xabari uchun joy -->
</div>
<!-- Input Container -->
<div class="input-container">
<div id="recording-mode" class="recording-mode">
<p class="text-center" data-translate-key="speakLangs"
style="font-size: 0.85rem; color: var(--text-secondary); letter-spacing: 0.5px;">
(GAPIRING: UZB / ENG / RUS)
</p>
<div class="lang-switcher">
<button class="lang-btn active" onclick="setLanguage('uz')">UZ</button>
<button class="lang-btn" onclick="setLanguage('ru')">RU</button>
<button class="lang-btn" onclick="setLanguage('en')">EN</button>
</div>
<div id="record-button-wrapper" class="record-button-wrapper">
<div class="visualizer-ring"></div>
<div class="visualizer-ring"></div>
<div class="visualizer-ring"></div>
<div class="visualizer-ring"></div>
<button id="record-button" class="record-button">
<i class="bi bi-mic-fill"></i>
</button>
</div>
<div class="recording-status">
<span id="recording-text" data-translate-key="recordInstruction">
Yordam so'rash uchun tugmani bosing
</span>
</div>
</div>
</div>
</div>
<script>
// ==================== GLOBAL VARIABLES ====================
let ws = null;
let mediaRecorder = null;
let audioChunks = [];
let isRecording = false;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const translations = {
uz: {
appTitle: 'Tez Tibbiy Yordam',
speakLangs: '(GAPIRING: UZB / ENG / RUS)',
connecting: 'Serverga ulanmoqda...',
connected: 'Ulandi',
disconnected: 'Uzildi',
connectionFailed: 'Ulanib bo\'lmadi. Sahifani yangilang.',
recordInstruction: 'Yordam so\'rash uchun tugmani bosing',
recording: 'Yozilmoqda... To\'xtatish uchun bosing',
micError: 'Mikrofonni ishlatib bo\'lmadi. Ruxsat bering.',
wsNotConnected: 'Server bilan aloqa yo\'q. Iltimos, kuting...',
welcomeMessage: 'Assalomu alaykum! Men sizning AI yordamchingizman. Sizga qanday yordam bera olaman?'
},
ru: {
appTitle: 'Скорая Помощь',
speakLangs: '(ГОВОРИТЕ: UZB / ENG / RUS)',
connecting: 'Соединение с сервером...',
connected: 'Подключено',
disconnected: 'Отключено',
connectionFailed: 'Не удалось подключиться. Обновите страницу.',
recordInstruction: 'Нажмите кнопку, чтобы позвать на помощь',
recording: 'Идет запись... Нажмите, чтобы остановить',
micError: 'Не удалось получить доступ к микрофону. Разрешите доступ.',
wsNotConnected: 'Нет соединения с сервером. Подождите...',
welcomeMessage: 'Здравствуйте! Я ваш AI-помощник. Чем я могу вам помочь?'
},
en: {
appTitle: 'Emergency Service',
speakLangs: '(SPEAK IN: UZB / ENG / RUS)',
connecting: 'Connecting to server...',
connected: 'Connected',
disconnected: 'Disconnected',
connectionFailed: 'Failed to connect. Please refresh the page.',
recordInstruction: 'Press the button to call for help',
recording: 'Recording... Press to stop',
micError: 'Could not access the microphone. Please grant permission.',
wsNotConnected: 'Not connected to the server. Please wait...',
welcomeMessage: 'Hello! I am your AI assistant. How can I help you?'
}
};
// ==================== DOM ELEMENTS ====================
const chatContainer = document.getElementById('chat-container');
const connectionStatusDiv = document.getElementById('connection-status');
const recordButton = document.getElementById('record-button');
const recordButtonWrapper = document.getElementById('record-button-wrapper');
const recordingText = document.getElementById('recording-text');
// ==================== LANGUAGE FUNCTIONALITY ====================
function setLanguage(lang) {
if (!translations[lang]) return;
document.documentElement.lang = lang;
document.querySelectorAll('[data-translate-key]').forEach(el => {
const key = el.dataset.translateKey;
if (translations[lang][key]) {
el.innerHTML = translations[lang][key];
}
});
document.querySelectorAll('.lang-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.innerText.toLowerCase() === lang) {
btn.classList.add('active');
}
});
localStorage.setItem('preferredLanguage', lang);
updateConnectionStatus(ws && ws.readyState === WebSocket.OPEN);
}
// ==================== WEBSOCKET CONNECTION ====================
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/chat`;
console.log('🔌 WebSocket ulanish:', wsUrl);
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('✅ WebSocket ulandi');
reconnectAttempts = 0;
updateConnectionStatus(true);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('📨 Xabar olindi:', data);
handleWebSocketMessage(data);
} catch (e) {
console.error('❌ Xabar parse xatoligi:', e);
}
};
ws.onerror = (error) => {
console.error('❌ WebSocket xatolik:', error);
updateConnectionStatus(false);
};
ws.onclose = () => {
console.log('📴 WebSocket uzildi');
updateConnectionStatus(false);
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
setTimeout(connectWebSocket, 3000);
} else {
const lang = localStorage.getItem('preferredLanguage') || 'uz';
connectionStatusDiv.querySelector('span').textContent = translations[lang].connectionFailed;
}
};
}
function updateConnectionStatus(connected) {
const lang = localStorage.getItem('preferredLanguage') || 'uz';
const statusText = connected ? translations[lang].connected : translations[lang].disconnected;
connectionStatusDiv.className = `connection-status ${connected ? 'connected' : 'disconnected'}`;
connectionStatusDiv.innerHTML = `<i class="bi bi-circle-fill" style="font-size: 0.6rem; margin-right: 6px;"></i> ${statusText}`;
}
// ==================== MESSAGE HANDLING ====================
function handleWebSocketMessage(data) {
hideTypingIndicator();
switch (data.type) {
case 'ai_response': addMessage('ai', data.text); break;
case 'audio_response': playAudioResponse(data.audio_url); break;
case 'transcription_result':
addMessage('user', data.text);
showTypingIndicator();
break;
case 'error': addMessage('ai', `❌ Xatolik: ${data.message}`); break;
default: console.log('📦 Noma\'lum xabar turi:', data.type);
}
}
function playAudioResponse(audioUrl) {
try {
console.log('🔊 Audio ijro etilmoqda:', audioUrl);
const audio = new Audio(audioUrl);
audio.play().catch(error => console.error('❌ Audio play xatoligi:', error));
} catch (error) {
console.error('❌ playAudioResponse xatoligi:', error);
}
}
function addMessage(sender, text, isWelcome = false) {
const typingIndicator = document.getElementById('typing-indicator');
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-robot"></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);
if (isWelcome && chatContainer.children.length > 0) {
chatContainer.insertBefore(messageDiv, chatContainer.firstChild);
} else if (typingIndicator) {
chatContainer.insertBefore(messageDiv, typingIndicator);
} else {
chatContainer.appendChild(messageDiv);
}
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function showTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
if (indicator) {
indicator.classList.add('show');
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}
function hideTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
if (indicator) {
indicator.classList.remove('show');
}
}
// ==================== VOICE RECORDING ====================
async function startRecording() {
const lang = localStorage.getItem('preferredLanguage') || 'uz';
try {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert(translations[lang].wsNotConnected);
return;
}
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
audioChunks = [];
mediaRecorder.ondataavailable = event => event.data.size > 0 && audioChunks.push(event.data);
mediaRecorder.onstop = () => {
sendAudioToServer(new Blob(audioChunks, { type: 'audio/webm' }));
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
isRecording = true;
recordButton.classList.add('recording');
recordButtonWrapper.classList.add('recording');
recordButton.innerHTML = '<i class="bi bi-stop-fill"></i>';
recordingText.textContent = translations[lang].recording;
} catch (error) {
console.error('❌ Mikrofon xatoligi:', error);
alert(translations[lang].micError);
}
}
function stopRecording() {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
isRecording = false;
const lang = localStorage.getItem('preferredLanguage') || 'uz';
recordButton.classList.remove('recording');
recordButtonWrapper.classList.remove('recording');
recordButton.innerHTML = '<i class="bi bi-mic-fill"></i>';
recordingText.textContent = translations[lang].recordInstruction;
showTypingIndicator();
}
}
async function sendAudioToServer(audioBlob) {
try {
const arrayBuffer = await audioBlob.arrayBuffer();
ws.send(new Uint8Array(arrayBuffer));
ws.send('__END__');
} catch (error) {
console.error('❌ Audio yuborishda xatolik:', error);
hideTypingIndicator();
}
}
// ==================== EVENT LISTENERS ====================
recordButton.addEventListener('click', () => isRecording ? stopRecording() : startRecording());
// ==================== INITIALIZATION ====================
function initialize() {
const savedLang = localStorage.getItem('preferredLanguage') || 'uz';
setLanguage(savedLang);
connectWebSocket();
const lang = localStorage.getItem('preferredLanguage') || 'uz';
const welcomeMessageHTML = `
<div class="message ai">
<div class="message-avatar"><i class="bi bi-robot"></i></div>
<div class="message-content">${translations[lang].welcomeMessage}</div>
</div>`;
const typingIndicatorHTML = `
<div id="typing-indicator" class="message ai typing-indicator">
<div class="message-avatar"><i class="bi bi-robot"></i></div>
<div class="message-content">
<div class="typing-dots">
<span></span><span></span><span></span>
</div>
</div>
</div>`;
chatContainer.innerHTML = welcomeMessageHTML + typingIndicatorHTML;
}
document.addEventListener('DOMContentLoaded', initialize);
</script>
</body>
</html>